├── .github ├── dependabot.yml └── workflows │ ├── flutter.yml │ └── remove-old-artifacts.yml ├── .gitignore ├── .metadata ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── hoc │ │ │ │ └── node_auth │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MyApp.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── bg.jpg └── user.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── app.dart ├── data │ ├── constants.dart │ ├── exception │ │ ├── local_data_source_exception.dart │ │ └── remote_data_source_exception.dart │ ├── local │ │ ├── entities │ │ │ ├── user_and_token_entity.dart │ │ │ ├── user_and_token_entity.g.dart │ │ │ ├── user_entity.dart │ │ │ └── user_entity.g.dart │ │ ├── local_data_source.dart │ │ ├── method_channel_crypto_impl.dart │ │ └── shared_pref_util.dart │ ├── mappers.dart │ ├── remote │ │ ├── api_service.dart │ │ ├── auth_interceptor.dart │ │ ├── remote_data_source.dart │ │ └── response │ │ │ ├── token_response.dart │ │ │ ├── token_response.g.dart │ │ │ ├── user_response.dart │ │ │ └── user_response.g.dart │ ├── serializers.dart │ ├── serializers.g.dart │ └── user_repository_imp.dart ├── domain │ ├── models │ │ ├── app_error.dart │ │ ├── auth_state.dart │ │ ├── auth_state.g.dart │ │ ├── user.dart │ │ ├── user.g.dart │ │ ├── user_and_token.dart │ │ └── user_and_token.g.dart │ ├── repositories │ │ └── user_repository.dart │ └── usecases │ │ ├── change_password_use_case.dart │ │ ├── get_auth_state_stream_use_case.dart │ │ ├── get_auth_state_use_case.dart │ │ ├── login_use_case.dart │ │ ├── logout_use_case.dart │ │ ├── register_use_case.dart │ │ ├── reset_password_use_case.dart │ │ ├── send_reset_password_email_use_case.dart │ │ └── upload_image_use_case.dart ├── main.dart ├── pages │ ├── home │ │ ├── change_password │ │ │ ├── change_password.dart │ │ │ ├── change_password_bloc.dart │ │ │ ├── change_password_bottomsheet.dart │ │ │ ├── change_password_state.dart │ │ │ └── change_password_state.g.dart │ │ ├── home.dart │ │ ├── home_bloc.dart │ │ ├── home_page.dart │ │ ├── home_profile_widget.dart │ │ └── home_state.dart │ ├── login │ │ ├── login.dart │ │ ├── login_bloc.dart │ │ ├── login_page.dart │ │ └── login_state.dart │ ├── register │ │ ├── register.dart │ │ ├── register_bloc.dart │ │ ├── register_page.dart │ │ └── register_state.dart │ └── reset_password │ │ ├── input_token │ │ ├── input_token_and_reset_password.dart │ │ ├── input_token_and_reset_password_bloc.dart │ │ └── input_token_and_reset_password_page.dart │ │ ├── reset_password_page.dart │ │ └── send_email │ │ ├── send_email.dart │ │ ├── send_email_bloc.dart │ │ ├── send_email_page.dart │ │ └── send_email_state.dart ├── utils │ ├── snackbar.dart │ ├── streams.dart │ ├── type_defs.dart │ ├── unit.dart │ └── validators.dart └── widgets │ └── password_textfield.dart ├── pubspec.lock ├── pubspec.yaml ├── renovate.json ├── screenshots ├── Screenshot1.png ├── Screenshot2.png ├── Screenshot3.png ├── Screenshot4.png ├── Screenshot5.png └── Screenshot6.png ├── test └── widget_test.dart ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── index.html └── manifest.json └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | enable-beta-ecosystems: true 8 | updates: 9 | - package-ecosystem: "pub" # See documentation for possible values 10 | directory: "/" # Location of package manifests 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /.github/workflows/flutter.yml: -------------------------------------------------------------------------------- 1 | name: Flutter 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | flutter-channel: [ 'stable' ] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: actions/setup-java@v4 21 | with: 22 | distribution: 'zulu' 23 | java-version: '17' 24 | 25 | - uses: subosito/flutter-action@v2 26 | with: 27 | channel: ${{ matrix.flutter-channel }} 28 | 29 | - name: Print Dart SDK version 30 | run: dart --version 31 | 32 | - name: Print Flutter SDK version 33 | run: flutter --version 34 | 35 | - name: Install dependencies 36 | run: dart pub get 37 | 38 | - name: Format code 39 | if: ${{ matrix.flutter-channel == 'stable' }} 40 | run: dart format lib --set-exit-if-changed 41 | 42 | - name: Analyze 43 | if: ${{ matrix.flutter-channel == 'stable' }} 44 | run: dart analyze lib 45 | 46 | - name: Gen code 47 | run: dart run build_runner build --delete-conflicting-outputs 48 | 49 | - name: Build Debug APK 50 | run: flutter build apk --debug --no-shrink 51 | 52 | - name: Upload APK 53 | if: ${{ matrix.flutter-channel == 'stable' }} 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: app-${{ matrix.flutter-channel }} 57 | path: build/app/outputs/apk/debug/app-debug.apk 58 | 59 | -------------------------------------------------------------------------------- /.github/workflows/remove-old-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Remove old artifacts 2 | 3 | on: 4 | # push: 5 | # branches: [ master ] 6 | 7 | schedule: 8 | # Runs at 01:00 UTC on the 1, 8, 15, 22 and 29th of every month. 9 | - cron: '0 1 */7 * *' 10 | 11 | workflow_dispatch: 12 | 13 | jobs: 14 | remove-old-artifacts: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 10 17 | 18 | steps: 19 | - name: Remove old artifacts 20 | uses: c-hive/gha-remove-artifacts@v1 21 | with: 22 | age: '7 days' 23 | skip-tags: true 24 | skip-recent: 5 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Exceptions to above rules. 44 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 45 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: b176042c1a8a9fdb5c96051b613895e7f0ab167b 8 | channel: master 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Flutter", 9 | "type": "dart", 10 | "request": "launch", 11 | "program": "lib/main.dart" 12 | }, 13 | { 14 | "name": "Flutter", 15 | "request": "launch", 16 | "type": "dart" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Petrus Nguyễn Thái Học 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node_auth # node-auth-flutter-BLoC-pattern-RxDart 2 | 3 | - ❄️❄️ `BLoC pattern` `rxdart` `stream` 🐋🐋 Simple auth app flutter, server node.js, BLoC pattern, RxDart 4 | - Functionalities: `LOGIN`, `LOGOUT`, `REGISTER`, `CHANGE PASSWORD`, `CHANGE AVATAR`, `FORGOT PASSWORD` 🌀🌀 5 | - **Pure rxdart BLoC pattern**. BLoC pattern without library. Flutter Functional & Reactive programming 🌱🌱 6 | - Platform-Specific Code With **Flutter Method Channel** 7 | - [Android](https://github.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/blob/master/android/app/src/main/kotlin/com/hoc/node_auth/MainActivity.kt#L21): using [Tink library](https://github.com/google/tink) to encrypt/decrypt access token and user info. 8 | - [iOS](https://github.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/blob/master/ios/Runner/AppDelegate.swift#L20): using [CryptoSwift library](https://github.com/krzyzanowskim/CryptoSwift) to encrypt/decrypt access token and user info. 9 | 10 | [![Flutter](https://github.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/actions/workflows/flutter.yml/badge.svg)](https://github.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/actions/workflows/flutter.yml) 11 | [![Codemagic build status](https://api.codemagic.io/apps/5e299cf863c55e0019edee46/5e299cf863c55e0019edee45/status_badge.svg)](https://codemagic.io/apps/5e299cf863c55e0019edee46/5e299cf863c55e0019edee45/latest_build) 12 | ![GitHub stars](https://img.shields.io/github/stars/hoc081098/node-auth-flutter-BLoC-pattern-RxDart?style=social) 13 | [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fhoc081098%2Fnode-auth-flutter-BLoC-pattern-RxDart&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com) 14 | 15 |
16 | Liked some of my work? Buy me a coffee (or more likely a beer) 17 | 18 | Buy Me A Coffee 19 | 20 | ## Video demo: 21 | 22 | [Youtube](https://youtu.be/OvsDKfy0aOs) 23 | 24 | ## Download apk [here](https://nightly.link/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/workflows/flutter/master/app.zip) 25 | 26 | ## Flutter version (stable channel) 27 | 28 | ```yaml 29 | environment: 30 | sdk: ^3.4.3 31 | flutter: ">=3.22.2" 32 | ``` 33 | 34 | ## Screenshots 35 | 36 | | | | | 37 | | :---: | :---: | :---: | 38 | | ![](screenshots/Screenshot1.png) | ![](screenshots/Screenshot2.png) | ![](screenshots/Screenshot3.png) 39 | | ![](screenshots/Screenshot4.png) | ![](screenshots/Screenshot5.png) | ![](screenshots/Screenshot6.png) 40 | 41 | ## Find this repository useful? ❤️ 42 | 43 | Star this repository and follow me for next creations! Thanks for your support 💗💗. 44 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | language: 5 | strict-casts: true 6 | strict-raw-types: true 7 | strict-inference: true 8 | 9 | linter: 10 | rules: 11 | - prefer_final_locals 12 | # https://github.com/dart-lang/lints#migrating-from-packagepedantic 13 | - always_declare_return_types 14 | - prefer_single_quotes 15 | - unawaited_futures 16 | - unsafe_html 17 | - avoid_slow_async_io -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | kotlin { 8 | jvmToolchain { 9 | languageVersion = JavaLanguageVersion.of(17) 10 | vendor = JvmVendorSpec.AZUL 11 | } 12 | } 13 | 14 | def localProperties = new Properties() 15 | def localPropertiesFile = rootProject.file('local.properties') 16 | if (localPropertiesFile.exists()) { 17 | localPropertiesFile.withReader('UTF-8') { reader -> 18 | localProperties.load(reader) 19 | } 20 | } 21 | 22 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 23 | if (flutterVersionCode == null) { 24 | flutterVersionCode = '1' 25 | } 26 | 27 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 28 | if (flutterVersionName == null) { 29 | flutterVersionName = '1.0' 30 | } 31 | 32 | android { 33 | compileSdkVersion 34 34 | namespace "com.hoc.node_auth" 35 | 36 | compileOptions { 37 | sourceCompatibility JavaVersion.VERSION_1_8 38 | targetCompatibility JavaVersion.VERSION_1_8 39 | } 40 | 41 | kotlinOptions { 42 | jvmTarget = '1.8' 43 | } 44 | 45 | sourceSets { 46 | main.java.srcDirs += 'src/main/kotlin' 47 | } 48 | 49 | defaultConfig { 50 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 51 | applicationId "com.hoc.node_auth" 52 | minSdkVersion 21 53 | targetSdkVersion 34 54 | versionCode flutterVersionCode.toInteger() 55 | versionName flutterVersionName 56 | multiDexEnabled true 57 | } 58 | 59 | buildTypes { 60 | release { 61 | // TODO: Add your own signing config for the release build. 62 | // Signing with the debug keys for now, so `flutter run --release` works. 63 | signingConfig signingConfigs.debug 64 | } 65 | } 66 | } 67 | 68 | flutter { 69 | source '../..' 70 | } 71 | 72 | dependencies { 73 | implementation "androidx.multidex:multidex:2.0.1" 74 | 75 | implementation "com.google.crypto.tink:tink-android:1.7.0" 76 | 77 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0' 78 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0' 79 | } 80 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 17 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/hoc/node_auth/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hoc.node_auth 2 | 3 | import android.util.Log 4 | import com.google.crypto.tink.subtle.Base64 5 | import io.flutter.embedding.android.FlutterActivity 6 | import io.flutter.embedding.engine.FlutterEngine 7 | import io.flutter.plugin.common.MethodCall 8 | import io.flutter.plugin.common.MethodChannel 9 | import kotlinx.coroutines.* 10 | 11 | class MainActivity : FlutterActivity() { 12 | private lateinit var cryptoChannel: MethodChannel 13 | private lateinit var mainScope: CoroutineScope 14 | 15 | //region Lifecycle 16 | override fun configureFlutterEngine(flutterEngine: FlutterEngine) { 17 | super.configureFlutterEngine(flutterEngine) 18 | Log.d("Flutter", "configureFlutterEngine flutterEngine=$flutterEngine $this") 19 | 20 | mainScope = MainScope() 21 | cryptoChannel = MethodChannel( 22 | flutterEngine.dartExecutor.binaryMessenger, 23 | CRYPTO_CHANNEL, 24 | ).apply { setMethodCallHandler(MethodCallHandlerImpl()) } 25 | } 26 | 27 | override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) { 28 | super.cleanUpFlutterEngine(flutterEngine) 29 | Log.d("Flutter", "cleanUpFlutterEngine flutterEngine=$flutterEngine $this") 30 | 31 | cryptoChannel.setMethodCallHandler(null) 32 | mainScope.cancel() 33 | } 34 | //endregion 35 | 36 | private inner class MethodCallHandlerImpl : MethodChannel.MethodCallHandler { 37 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 38 | when (call.method) { 39 | ENCRYPT_METHOD -> encrypt(call, result) 40 | DECRYPT_METHOD -> decrypt(call, result) 41 | else -> result.notImplemented() 42 | } 43 | } 44 | } 45 | 46 | //region Handlers 47 | private fun encrypt( 48 | call: MethodCall, 49 | result: MethodChannel.Result 50 | ) { 51 | val plaintext = checkNotNull(call.arguments()) { "plaintext must be not null" } 52 | 53 | mainScope.launch { 54 | runCatching { 55 | withContext(Dispatchers.IO) { 56 | plaintext 57 | .encodeToByteArray() 58 | .let { myApp.aead.encrypt(it, null) } 59 | .let { Base64.encode(it) } 60 | } 61 | } 62 | .onSuccess { result.success(it) } 63 | .onFailureExceptCancellationException { 64 | Log.e("Flutter", "encrypt", it) 65 | result.error(CRYPTO_ERROR_CODE, it.message, null) 66 | } 67 | } 68 | } 69 | 70 | private fun decrypt( 71 | call: MethodCall, 72 | result: MethodChannel.Result 73 | ) { 74 | val ciphertext = checkNotNull(call.arguments()) { "ciphertext must be not null" } 75 | 76 | mainScope.launch { 77 | runCatching { 78 | withContext(Dispatchers.IO) { 79 | Base64 80 | .decode(ciphertext, Base64.DEFAULT) 81 | .let { myApp.aead.decrypt(it, null) } 82 | .decodeToString() 83 | } 84 | } 85 | .onSuccess { result.success(it) } 86 | .onFailureExceptCancellationException { 87 | Log.e("Flutter", "decrypt", it) 88 | result.error(CRYPTO_ERROR_CODE, it.message, null) 89 | } 90 | } 91 | } 92 | //endregion 93 | 94 | private companion object { 95 | const val CRYPTO_CHANNEL = "com.hoc.node_auth/crypto" 96 | const val CRYPTO_ERROR_CODE = "com.hoc.node_auth/crypto_error" 97 | const val ENCRYPT_METHOD = "encrypt" 98 | const val DECRYPT_METHOD = "decrypt" 99 | } 100 | } 101 | 102 | private inline fun Result.onFailureExceptCancellationException(action: (throwable: Throwable) -> Unit): Result { 103 | return onFailure { 104 | if (it is CancellationException) throw it 105 | action(it) 106 | } 107 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/hoc/node_auth/MyApp.kt: -------------------------------------------------------------------------------- 1 | package com.hoc.node_auth 2 | 3 | import android.app.Activity 4 | import androidx.multidex.MultiDex 5 | import com.google.crypto.tink.Aead 6 | import com.google.crypto.tink.KeyTemplates 7 | import com.google.crypto.tink.aead.AeadConfig 8 | import com.google.crypto.tink.integration.android.AndroidKeysetManager 9 | import com.google.crypto.tink.integration.android.AndroidKeystoreKmsClient 10 | import io.flutter.app.FlutterApplication 11 | 12 | class MyApp : FlutterApplication() { 13 | val aead: Aead by lazy { 14 | AndroidKeysetManager 15 | .Builder() 16 | .withSharedPref(this, KEYSET_NAME, PREF_FILE_NAME) 17 | .withKeyTemplate(KeyTemplates.get("AES256_GCM")) 18 | .withMasterKeyUri(MASTER_KEY_URI) 19 | .build() 20 | .keysetHandle 21 | .getPrimitive(Aead::class.java) 22 | } 23 | 24 | override fun onCreate() { 25 | super.onCreate() 26 | MultiDex.install(this) 27 | AeadConfig.register() 28 | } 29 | 30 | private companion object { 31 | private const val KEYSET_NAME = "nodeauth_keyset" 32 | private const val PREF_FILE_NAME = "nodeauth_pref" 33 | private const val MASTER_KEY_URI = "${AndroidKeystoreKmsClient.PREFIX}nodeauth_master_key" 34 | } 35 | } 36 | 37 | val Activity.myApp: MyApp get() = application as MyApp -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.7.2" apply false 22 | id "org.jetbrains.kotlin.android" version "2.0.21" apply false 23 | id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0") 24 | } 25 | 26 | rootProject.name = "github_search" 27 | include ":app" -------------------------------------------------------------------------------- /assets/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/assets/bg.jpg -------------------------------------------------------------------------------- /assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/assets/user.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | 36 | pod 'CryptoSwift', '~> 1.2.0' 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_ios_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - CryptoSwift (1.2.0) 3 | - cupertino_http (0.0.1): 4 | - Flutter 5 | - Flutter (1.0.0) 6 | - image_picker_ios (0.0.1): 7 | - Flutter 8 | - shared_preferences_ios (0.0.1): 9 | - Flutter 10 | 11 | DEPENDENCIES: 12 | - CryptoSwift (~> 1.2.0) 13 | - cupertino_http (from `.symlinks/plugins/cupertino_http/ios`) 14 | - Flutter (from `Flutter`) 15 | - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) 16 | - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) 17 | 18 | SPEC REPOS: 19 | trunk: 20 | - CryptoSwift 21 | 22 | EXTERNAL SOURCES: 23 | cupertino_http: 24 | :path: ".symlinks/plugins/cupertino_http/ios" 25 | Flutter: 26 | :path: Flutter 27 | image_picker_ios: 28 | :path: ".symlinks/plugins/image_picker_ios/ios" 29 | shared_preferences_ios: 30 | :path: ".symlinks/plugins/shared_preferences_ios/ios" 31 | 32 | SPEC CHECKSUMS: 33 | CryptoSwift: 40e374e45291d8dceedcb0d6184da94533eaabdf 34 | cupertino_http: 5f8b1161107fe6c8d94a0c618735a033d93fa7db 35 | Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a 36 | image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb 37 | shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad 38 | 39 | PODFILE CHECKSUM: 28949384d1a9817c5c0092fcf6ffc3a836337eb0 40 | 41 | COCOAPODS: 1.11.2 42 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | import CryptoSwift 4 | 5 | private extension String { 6 | static let CRYPTO_CHANNEL = "com.hoc.node_auth/crypto" 7 | static let CRYPTO_ERROR_CODE = "com.hoc.node_auth/crypto_error" 8 | static let ENCRYPT_METHOD = "encrypt" 9 | static let DECRYPT_METHOD = "decrypt" 10 | } 11 | 12 | @UIApplicationMain 13 | @objc class AppDelegate: FlutterAppDelegate { 14 | override func application( 15 | _ application: UIApplication, 16 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 17 | ) -> Bool { 18 | let flutterVC = window?.rootViewController as! FlutterViewController 19 | 20 | let cryptoChannel = FlutterMethodChannel( 21 | name: .CRYPTO_CHANNEL, 22 | binaryMessenger: flutterVC.binaryMessenger 23 | ) 24 | cryptoChannel.setMethodCallHandler { call, result in 25 | switch call.method { 26 | case .ENCRYPT_METHOD: encrypt(call: call, result: result) 27 | case .DECRYPT_METHOD: decrypt(call: call, result: result) 28 | default: 29 | result(FlutterMethodNotImplemented) 30 | } 31 | } 32 | 33 | GeneratedPluginRegistrant.register(with: self) 34 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 35 | } 36 | } 37 | 38 | private enum AESConfig { 39 | static let iv: [UInt8] = "_hoc081098_auth_".bytes 40 | static let key: [UInt8] = "__hoc081098_nodejs_auth_rxdart__".bytes 41 | 42 | static let backgroundQueue = DispatchQueue.global(qos: .userInitiated) 43 | 44 | static func gcm() -> GCM { GCM(iv: AESConfig.iv, mode: .combined) } 45 | } 46 | 47 | private func complete(result: @escaping FlutterResult, with error: Error) { 48 | NSLog("\n[NODE_AUTH] Error: \(error)") 49 | 50 | executeOnMain { 51 | result( 52 | FlutterError( 53 | code: .CRYPTO_ERROR_CODE, 54 | message: error.localizedDescription, 55 | details: nil 56 | ) 57 | ) 58 | } 59 | } 60 | 61 | private func executeOnMain(block: @escaping () -> Void) { 62 | if Thread.isMainThread { 63 | block() 64 | } else { 65 | DispatchQueue.main.async { 66 | block() 67 | } 68 | } 69 | } 70 | 71 | private func useAES( 72 | input: String, 73 | result: @escaping FlutterResult, 74 | inputToBytes: (String) -> [UInt8]?, 75 | bytesToString: @escaping ([UInt8]) -> String?, 76 | block: @escaping (AES, [UInt8]) throws -> [UInt8] 77 | ) { 78 | guard let inputBytes = inputToBytes(input) else { 79 | NSLog("\n[NODE_AUTH] Error: inputToBytes returns nil") 80 | 81 | executeOnMain { 82 | result( 83 | FlutterError( 84 | code: .CRYPTO_ERROR_CODE, 85 | message: "An unexpected error occurred!", 86 | details: nil 87 | ) 88 | ) 89 | } 90 | return 91 | } 92 | 93 | AESConfig.backgroundQueue.async { 94 | let start = DispatchTime.now() 95 | 96 | do { 97 | let aes = try AES( 98 | key: AESConfig.key, 99 | blockMode: AESConfig.gcm(), 100 | padding: .noPadding 101 | ) 102 | 103 | let outputBytes = try block(aes, inputBytes) 104 | guard let stringResult = bytesToString(outputBytes) else { 105 | NSLog("\n[NODE_AUTH] Error: bytesToString returns nil") 106 | 107 | executeOnMain { 108 | result( 109 | FlutterError( 110 | code: .CRYPTO_ERROR_CODE, 111 | message: "An unexpected error occurred!", 112 | details: nil 113 | ) 114 | ) 115 | } 116 | return 117 | } 118 | 119 | let end = DispatchTime.now() 120 | let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds 121 | let millisTime = Double(nanoTime) / 1_000_000 122 | NSLog("\n[NODE_AUTH] Time: \(millisTime) ms") 123 | 124 | executeOnMain { result(stringResult) } 125 | } catch { 126 | complete(result: result, with: error) 127 | } 128 | } 129 | } 130 | 131 | private func encrypt(call: FlutterMethodCall, result: @escaping FlutterResult) { 132 | useAES( 133 | input: call.arguments as! String, 134 | result: result, 135 | inputToBytes: { $0.bytes }, 136 | bytesToString: base64Encode(bytes:) 137 | ) { aes, bytes in try aes.encrypt(bytes) } 138 | } 139 | 140 | 141 | private func decrypt(call: FlutterMethodCall, result: @escaping FlutterResult) { 142 | useAES( 143 | input: call.arguments as! String, 144 | result: result, 145 | inputToBytes: base64Decode(s:), 146 | bytesToString: { .init(bytes: $0, encoding: .utf8) } 147 | ) { aes, bytes in try aes.decrypt(bytes) } 148 | } 149 | 150 | func base64Decode(s: String) -> [UInt8]? { 151 | Data(base64Encoded: s)?.bytes 152 | } 153 | 154 | func base64Encode(bytes: [UInt8]) -> String { 155 | Data(bytes).base64EncodedString() 156 | } 157 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPhotoLibraryUsageDescription 6 | This app needs access to your photos 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | node_auth 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart'; 3 | import 'package:flutter_disposebag/flutter_disposebag.dart'; 4 | import 'package:flutter_provider/flutter_provider.dart'; 5 | import 'package:node_auth/domain/models/app_error.dart'; 6 | import 'package:node_auth/domain/models/auth_state.dart'; 7 | import 'package:node_auth/domain/repositories/user_repository.dart'; 8 | import 'package:node_auth/domain/usecases/get_auth_state_stream_use_case.dart'; 9 | import 'package:node_auth/domain/usecases/get_auth_state_use_case.dart'; 10 | import 'package:node_auth/domain/usecases/login_use_case.dart'; 11 | import 'package:node_auth/domain/usecases/logout_use_case.dart'; 12 | import 'package:node_auth/domain/usecases/register_use_case.dart'; 13 | import 'package:node_auth/domain/usecases/upload_image_use_case.dart'; 14 | import 'package:node_auth/pages/home/home.dart'; 15 | import 'package:node_auth/pages/login/login.dart'; 16 | import 'package:node_auth/pages/register/register.dart'; 17 | import 'package:node_auth/pages/reset_password/reset_password_page.dart'; 18 | import 'package:node_auth/utils/streams.dart'; 19 | 20 | class MyApp extends StatelessWidget { 21 | const MyApp({super.key}); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | final routes = { 26 | Navigator.defaultRouteName: (context) { 27 | return Provider.factory( 28 | (context) => GetAuthStateUseCase(context.get()), 29 | child: const Home(), 30 | ); 31 | }, 32 | RegisterPage.routeName: (context) { 33 | return BlocProvider( 34 | initBloc: (context) => RegisterBloc( 35 | RegisterUseCase(context.get()), 36 | ), 37 | child: const RegisterPage(), 38 | ); 39 | }, 40 | HomePage.routeName: (context) { 41 | return BlocProvider( 42 | initBloc: (context) { 43 | final userRepository = context.get(); 44 | return HomeBloc( 45 | LogoutUseCase(userRepository), 46 | GetAuthStateStreamUseCase(userRepository), 47 | UploadImageUseCase(userRepository), 48 | ); 49 | }, 50 | child: const HomePage(), 51 | ); 52 | }, 53 | LoginPage.routeName: (context) { 54 | return BlocProvider( 55 | initBloc: (context) => LoginBloc( 56 | LoginUseCase(context.get()), 57 | ), 58 | child: const LoginPage(), 59 | ); 60 | }, 61 | ResetPasswordPage.routeName: (context) { 62 | return const ResetPasswordPage(); 63 | }, 64 | }; 65 | 66 | final themeData = ThemeData(brightness: Brightness.dark); 67 | return Provider>.value( 68 | routes, 69 | child: MaterialApp( 70 | title: 'Flutter Demo', 71 | theme: themeData.copyWith( 72 | colorScheme: themeData.colorScheme.copyWith( 73 | secondary: const Color(0xFF00e676), 74 | ), 75 | ), 76 | routes: routes, 77 | debugShowCheckedModeBanner: false, 78 | ), 79 | ); 80 | } 81 | } 82 | 83 | class Home extends StatefulWidget { 84 | const Home({super.key}); 85 | 86 | @override 87 | State createState() => _HomeState(); 88 | } 89 | 90 | class _HomeState extends State with DisposeBagMixin { 91 | late final StateStream?> authState$; 92 | 93 | @override 94 | void initState() { 95 | super.initState(); 96 | 97 | final getAuthState = Provider.of(context); 98 | authState$ = getAuthState().castAsNullable().publishState(null) 99 | ..connect().disposedBy(bag); 100 | } 101 | 102 | @override 103 | Widget build(BuildContext context) { 104 | final routes = Provider.of>(context); 105 | 106 | return RxStreamBuilder?>( 107 | stream: authState$, 108 | builder: (context, result) { 109 | if (result == null) { 110 | debugPrint('[HOME] home [1] >> [waiting...]'); 111 | 112 | return Container( 113 | width: double.infinity, 114 | height: double.infinity, 115 | color: Theme.of(context).cardColor, 116 | child: const Center( 117 | child: CircularProgressIndicator( 118 | valueColor: AlwaysStoppedAnimation(Colors.white), 119 | ), 120 | ), 121 | ); 122 | } 123 | 124 | return result.fold( 125 | ifLeft: (appError) { 126 | debugPrint( 127 | '[HOME] home [2] >> [error -> NotAuthenticated] error=$appError'); 128 | return routes[LoginPage.routeName]!(context); 129 | }, 130 | ifRight: (authState) { 131 | if (authState is UnauthenticatedState) { 132 | debugPrint('[HOME] home [3] >> [Unauthenticated]'); 133 | return routes[LoginPage.routeName]!(context); 134 | } 135 | 136 | if (authState is AuthenticatedState) { 137 | debugPrint('[HOME] home [4] >> [Authenticated]'); 138 | return routes[HomePage.routeName]!(context); 139 | } 140 | 141 | throw StateError('Unknown auth state: $authState'); 142 | }, 143 | ); 144 | }, 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/data/constants.dart: -------------------------------------------------------------------------------- 1 | const baseUrl = 'node-auth-081098.onrender.com'; 2 | -------------------------------------------------------------------------------- /lib/data/exception/local_data_source_exception.dart: -------------------------------------------------------------------------------- 1 | class LocalDataSourceException implements Exception { 2 | final String message; 3 | final Object error; 4 | final StackTrace stackTrace; 5 | 6 | const LocalDataSourceException(this.message, this.error, this.stackTrace); 7 | 8 | @override 9 | String toString() => 10 | 'LocalDataSourceException{message=$message, error=$error, stackTrace=$stackTrace}'; 11 | } 12 | -------------------------------------------------------------------------------- /lib/data/exception/remote_data_source_exception.dart: -------------------------------------------------------------------------------- 1 | class RemoteDataSourceException implements Exception { 2 | final String message; 3 | final Object error; 4 | final StackTrace stackTrace; 5 | 6 | const RemoteDataSourceException(this.message, this.error, this.stackTrace); 7 | 8 | @override 9 | String toString() => 10 | 'RemoteDataSourceException{message=$message, error=$error, stackTrace=$stackTrace}'; 11 | } 12 | -------------------------------------------------------------------------------- /lib/data/local/entities/user_and_token_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:node_auth/data/local/entities/user_entity.dart'; 4 | import 'package:node_auth/data/serializers.dart'; 5 | 6 | part 'user_and_token_entity.g.dart'; 7 | 8 | abstract class UserAndTokenEntity 9 | implements Built { 10 | @BuiltValueField(wireName: 'token') 11 | String get token; 12 | 13 | @BuiltValueField(wireName: 'user') 14 | UserEntity get user; 15 | 16 | static Serializer get serializer => 17 | _$userAndTokenEntitySerializer; 18 | 19 | UserAndTokenEntity._(); 20 | 21 | factory UserAndTokenEntity( 22 | [void Function(UserAndTokenEntityBuilder) updates]) = 23 | _$UserAndTokenEntity; 24 | 25 | factory UserAndTokenEntity.fromJson(Map json) => 26 | serializers.deserializeWith(serializer, json)!; 27 | 28 | Map toJson() => 29 | serializers.serializeWith(serializer, this) as Map; 30 | } 31 | -------------------------------------------------------------------------------- /lib/data/local/entities/user_and_token_entity.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user_and_token_entity.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | Serializer _$userAndTokenEntitySerializer = 10 | new _$UserAndTokenEntitySerializer(); 11 | 12 | class _$UserAndTokenEntitySerializer 13 | implements StructuredSerializer { 14 | @override 15 | final Iterable types = const [UserAndTokenEntity, _$UserAndTokenEntity]; 16 | @override 17 | final String wireName = 'UserAndTokenEntity'; 18 | 19 | @override 20 | Iterable serialize( 21 | Serializers serializers, UserAndTokenEntity object, 22 | {FullType specifiedType = FullType.unspecified}) { 23 | final result = [ 24 | 'token', 25 | serializers.serialize(object.token, 26 | specifiedType: const FullType(String)), 27 | 'user', 28 | serializers.serialize(object.user, 29 | specifiedType: const FullType(UserEntity)), 30 | ]; 31 | 32 | return result; 33 | } 34 | 35 | @override 36 | UserAndTokenEntity deserialize( 37 | Serializers serializers, Iterable serialized, 38 | {FullType specifiedType = FullType.unspecified}) { 39 | final result = new UserAndTokenEntityBuilder(); 40 | 41 | final iterator = serialized.iterator; 42 | while (iterator.moveNext()) { 43 | final key = iterator.current! as String; 44 | iterator.moveNext(); 45 | final Object? value = iterator.current; 46 | switch (key) { 47 | case 'token': 48 | result.token = serializers.deserialize(value, 49 | specifiedType: const FullType(String))! as String; 50 | break; 51 | case 'user': 52 | result.user.replace(serializers.deserialize(value, 53 | specifiedType: const FullType(UserEntity))! as UserEntity); 54 | break; 55 | } 56 | } 57 | 58 | return result.build(); 59 | } 60 | } 61 | 62 | class _$UserAndTokenEntity extends UserAndTokenEntity { 63 | @override 64 | final String token; 65 | @override 66 | final UserEntity user; 67 | 68 | factory _$UserAndTokenEntity( 69 | [void Function(UserAndTokenEntityBuilder)? updates]) => 70 | (new UserAndTokenEntityBuilder()..update(updates))._build(); 71 | 72 | _$UserAndTokenEntity._({required this.token, required this.user}) 73 | : super._() { 74 | BuiltValueNullFieldError.checkNotNull( 75 | token, r'UserAndTokenEntity', 'token'); 76 | BuiltValueNullFieldError.checkNotNull(user, r'UserAndTokenEntity', 'user'); 77 | } 78 | 79 | @override 80 | UserAndTokenEntity rebuild( 81 | void Function(UserAndTokenEntityBuilder) updates) => 82 | (toBuilder()..update(updates)).build(); 83 | 84 | @override 85 | UserAndTokenEntityBuilder toBuilder() => 86 | new UserAndTokenEntityBuilder()..replace(this); 87 | 88 | @override 89 | bool operator ==(Object other) { 90 | if (identical(other, this)) return true; 91 | return other is UserAndTokenEntity && 92 | token == other.token && 93 | user == other.user; 94 | } 95 | 96 | @override 97 | int get hashCode { 98 | var _$hash = 0; 99 | _$hash = $jc(_$hash, token.hashCode); 100 | _$hash = $jc(_$hash, user.hashCode); 101 | _$hash = $jf(_$hash); 102 | return _$hash; 103 | } 104 | 105 | @override 106 | String toString() { 107 | return (newBuiltValueToStringHelper(r'UserAndTokenEntity') 108 | ..add('token', token) 109 | ..add('user', user)) 110 | .toString(); 111 | } 112 | } 113 | 114 | class UserAndTokenEntityBuilder 115 | implements Builder { 116 | _$UserAndTokenEntity? _$v; 117 | 118 | String? _token; 119 | String? get token => _$this._token; 120 | set token(String? token) => _$this._token = token; 121 | 122 | UserEntityBuilder? _user; 123 | UserEntityBuilder get user => _$this._user ??= new UserEntityBuilder(); 124 | set user(UserEntityBuilder? user) => _$this._user = user; 125 | 126 | UserAndTokenEntityBuilder(); 127 | 128 | UserAndTokenEntityBuilder get _$this { 129 | final $v = _$v; 130 | if ($v != null) { 131 | _token = $v.token; 132 | _user = $v.user.toBuilder(); 133 | _$v = null; 134 | } 135 | return this; 136 | } 137 | 138 | @override 139 | void replace(UserAndTokenEntity other) { 140 | ArgumentError.checkNotNull(other, 'other'); 141 | _$v = other as _$UserAndTokenEntity; 142 | } 143 | 144 | @override 145 | void update(void Function(UserAndTokenEntityBuilder)? updates) { 146 | if (updates != null) updates(this); 147 | } 148 | 149 | @override 150 | UserAndTokenEntity build() => _build(); 151 | 152 | _$UserAndTokenEntity _build() { 153 | _$UserAndTokenEntity _$result; 154 | try { 155 | _$result = _$v ?? 156 | new _$UserAndTokenEntity._( 157 | token: BuiltValueNullFieldError.checkNotNull( 158 | token, r'UserAndTokenEntity', 'token'), 159 | user: user.build()); 160 | } catch (_) { 161 | late String _$failedField; 162 | try { 163 | _$failedField = 'user'; 164 | user.build(); 165 | } catch (e) { 166 | throw new BuiltValueNestedFieldError( 167 | r'UserAndTokenEntity', _$failedField, e.toString()); 168 | } 169 | rethrow; 170 | } 171 | replace(_$result); 172 | return _$result; 173 | } 174 | } 175 | 176 | // ignore_for_file: deprecated_member_use_from_same_package,type=lint 177 | -------------------------------------------------------------------------------- /lib/data/local/entities/user_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:node_auth/data/serializers.dart'; 4 | 5 | part 'user_entity.g.dart'; 6 | 7 | abstract class UserEntity implements Built { 8 | @BuiltValueField(wireName: 'name') 9 | String get name; 10 | 11 | @BuiltValueField(wireName: 'email') 12 | String get email; 13 | 14 | @BuiltValueField(wireName: 'created_at') 15 | DateTime get createdAt; 16 | 17 | @BuiltValueField(wireName: 'image_url') 18 | String? get imageUrl; 19 | 20 | static Serializer get serializer => _$userEntitySerializer; 21 | 22 | UserEntity._(); 23 | 24 | factory UserEntity([void Function(UserEntityBuilder) updates]) = _$UserEntity; 25 | 26 | factory UserEntity.fromJson(Map json) => 27 | serializers.deserializeWith(serializer, json)!; 28 | 29 | Map toJson() => 30 | serializers.serializeWith(serializer, this) as Map; 31 | } 32 | -------------------------------------------------------------------------------- /lib/data/local/local_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:node_auth/data/local/entities/user_and_token_entity.dart'; 2 | import 'package:node_auth/domain/models/app_error.dart'; 3 | 4 | abstract class LocalDataSource { 5 | /// Returns a single-subscription stream that emits [UserAndTokenEntity] or null 6 | Stream get userAndToken$; 7 | 8 | /// Returns a future that completes with a [UserAndTokenEntity] value or null 9 | Single get userAndToken; 10 | 11 | /// Save [userAndToken] into local storage. 12 | /// Throws [LocalDataSourceException] if saving is failed 13 | Single saveUserAndToken(UserAndTokenEntity userAndToken); 14 | 15 | /// Remove user and token from local storage. 16 | /// Throws [LocalDataSourceException] if removing is failed 17 | Single removeUserAndToken(); 18 | } 19 | 20 | abstract class Crypto { 21 | Future encrypt(String plaintext); 22 | 23 | Future decrypt(String ciphertext); 24 | } 25 | -------------------------------------------------------------------------------- /lib/data/local/method_channel_crypto_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:node_auth/data/exception/local_data_source_exception.dart'; 3 | import 'package:node_auth/data/local/local_data_source.dart'; 4 | 5 | class MethodChannelCryptoImpl implements Crypto { 6 | static const cryptoChannel = 'com.hoc.node_auth/crypto'; 7 | static const cryptoErrorCode = 'com.hoc.node_auth/crypto_error'; 8 | static const encryptMethod = 'encrypt'; 9 | static const decryptMethod = 'decrypt'; 10 | static const MethodChannel channel = MethodChannel(cryptoChannel); 11 | 12 | @override 13 | Future encrypt(String plaintext) => channel 14 | .invokeMethod(encryptMethod, plaintext) 15 | .then((v) => v!) 16 | .onError((e, s) => plaintext) 17 | .onError((e, s) => 18 | throw LocalDataSourceException('Cannot encrypt the plaintext', e, s)); 19 | 20 | @override 21 | Future decrypt(String ciphertext) => channel 22 | .invokeMethod(decryptMethod, ciphertext) 23 | .then((v) => v!) 24 | .onError((e, s) => ciphertext) 25 | .onError((e, s) => throw LocalDataSourceException( 26 | 'Cannot decrypt the ciphertext', e, s)); 27 | } 28 | -------------------------------------------------------------------------------- /lib/data/local/shared_pref_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:node_auth/data/exception/local_data_source_exception.dart'; 5 | import 'package:node_auth/data/local/entities/user_and_token_entity.dart'; 6 | import 'package:node_auth/data/local/local_data_source.dart'; 7 | import 'package:rx_shared_preferences/rx_shared_preferences.dart'; 8 | import 'package:rxdart_ext/rxdart_ext.dart'; 9 | 10 | class SharedPrefUtil implements LocalDataSource { 11 | static const _kUserTokenKey = 'com.hoc.node_auth_flutter.user_and_token'; 12 | final RxSharedPreferences _rxPrefs; 13 | final Crypto _crypto; 14 | 15 | const SharedPrefUtil(this._rxPrefs, this._crypto); 16 | 17 | @override 18 | Single removeUserAndToken() => Single.fromCallable( 19 | () => _rxPrefs.remove(_kUserTokenKey).onError((e, s) => 20 | throw LocalDataSourceException( 21 | 'Cannot delete user and token', e, s)), 22 | ); 23 | 24 | @override 25 | Single saveUserAndToken(UserAndTokenEntity userAndToken) => 26 | Single.fromCallable( 27 | () => _rxPrefs 28 | .write(_kUserTokenKey, userAndToken, _toString) 29 | .onError((e, s) => throw LocalDataSourceException( 30 | 'Cannot save user and token', e, s)), 31 | ); 32 | 33 | @override 34 | Single get userAndToken => Single.fromCallable( 35 | () => _rxPrefs 36 | .read(_kUserTokenKey, _toEntity) 37 | .onError((e, s) => throw LocalDataSourceException( 38 | 'Cannot read user and token', e, s)), 39 | ); 40 | 41 | @override 42 | Stream get userAndToken$ => _rxPrefs 43 | .observe(_kUserTokenKey, _toEntity) 44 | .onErrorReturnWith((e, s) => 45 | throw LocalDataSourceException('Cannot read user and token', e, s)); 46 | 47 | // 48 | // Encoder and Decoder 49 | // 50 | 51 | FutureOr _toEntity(dynamic jsonString) => jsonString == 52 | null 53 | ? null 54 | : _crypto.decrypt(jsonString as String).then((s) => 55 | UserAndTokenEntity.fromJson(jsonDecode(s) as Map)); 56 | 57 | FutureOr _toString(UserAndTokenEntity? entity) => 58 | entity == null ? null : _crypto.encrypt(jsonEncode(entity)); 59 | } 60 | -------------------------------------------------------------------------------- /lib/data/mappers.dart: -------------------------------------------------------------------------------- 1 | part of 'user_repository_imp.dart'; 2 | 3 | abstract class _Mappers { 4 | /// 5 | /// Convert error to [Failure] 6 | /// 7 | static AppError errorToAppError(Object e, StackTrace s) { 8 | if (e is CancellationException) { 9 | return const AppCancellationError(); 10 | } 11 | 12 | if (e is RemoteDataSourceException) { 13 | if (e.error is CancellationException) { 14 | return const AppCancellationError(); 15 | } 16 | return AppError( 17 | message: e.message, 18 | error: e, 19 | stackTrace: s, 20 | ); 21 | } 22 | 23 | if (e is LocalDataSourceException) { 24 | if (e.error is CancellationException) { 25 | return const AppCancellationError(); 26 | } 27 | return AppError( 28 | message: e.message, 29 | error: e, 30 | stackTrace: s, 31 | ); 32 | } 33 | 34 | return AppError( 35 | message: e.toString(), 36 | error: e, 37 | stackTrace: s, 38 | ); 39 | } 40 | 41 | /// Entity -> Domain 42 | static AuthenticationState userAndTokenEntityToDomainAuthState( 43 | UserAndTokenEntity? entity) { 44 | if (entity == null) { 45 | return UnauthenticatedState(); 46 | } 47 | 48 | final userAndTokenBuilder = UserAndTokenBuilder() 49 | ..user = _Mappers.userEntityToUserDomain(entity.user) 50 | ..token = entity.token; 51 | 52 | return AuthenticatedState((b) => b.userAndToken = userAndTokenBuilder); 53 | } 54 | 55 | /// Entity -> Domain 56 | static UserBuilder userEntityToUserDomain(UserEntity userEntity) { 57 | return UserBuilder() 58 | ..name = userEntity.name 59 | ..email = userEntity.email 60 | ..createdAt = userEntity.createdAt 61 | ..imageUrl = userEntity.imageUrl; 62 | } 63 | 64 | /// Response -> Entity 65 | static UserEntityBuilder userResponseToUserEntity(UserResponse userResponse) { 66 | return UserEntityBuilder() 67 | ..name = userResponse.name 68 | ..email = userResponse.email 69 | ..createdAt = userResponse.createdAt 70 | ..imageUrl = userResponse.imageUrl; 71 | } 72 | 73 | /// Response -> Entity 74 | static UserAndTokenEntity userResponseToUserAndTokenEntity( 75 | UserResponse user, 76 | String token, 77 | ) { 78 | return UserAndTokenEntity( 79 | (b) => b 80 | ..token = token 81 | ..user = userResponseToUserEntity(user), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/data/remote/auth_interceptor.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_function_declarations_over_variables 2 | 3 | import 'dart:io'; 4 | 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:http_client_hoc081098/http_client_hoc081098.dart'; 7 | 8 | class AuthInterceptor { 9 | final Future Function() onUnauthorized; 10 | 11 | AuthInterceptor({ 12 | required this.onUnauthorized, 13 | }); 14 | 15 | late final RequestInterceptor requestInterceptor = (request) => request; 16 | 17 | late final ResponseInterceptor responseInterceptor = 18 | (request, response) async { 19 | if (response.statusCode == HttpStatus.unauthorized) { 20 | debugPrint( 21 | '[AUTH_INTERCEPTOR] {interceptor} 401 - unauthenticated error ===> login again'); 22 | await onUnauthorized(); 23 | } 24 | 25 | return response; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /lib/data/remote/remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:node_auth/data/remote/response/token_response.dart'; 4 | import 'package:node_auth/data/remote/response/user_response.dart'; 5 | import 'package:node_auth/domain/models/app_error.dart'; 6 | 7 | abstract class RemoteDataSource { 8 | Single loginUser(String email, String password); 9 | 10 | Single registerUser( 11 | String name, 12 | String email, 13 | String password, 14 | ); 15 | 16 | Single changePassword( 17 | String email, 18 | String password, 19 | String newPassword, 20 | String token, 21 | ); 22 | 23 | Single resetPassword( 24 | String email, { 25 | String? token, 26 | String? newPassword, 27 | }); 28 | 29 | Single getUserProfile(String email, String token); 30 | 31 | Single uploadImage( 32 | File file, 33 | String email, 34 | String token, 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /lib/data/remote/response/token_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:node_auth/data/serializers.dart'; 4 | 5 | part 'token_response.g.dart'; 6 | 7 | abstract class TokenResponse 8 | implements Built { 9 | String? get token; 10 | 11 | String get message; 12 | 13 | static Serializer get serializer => _$tokenResponseSerializer; 14 | 15 | TokenResponse._(); 16 | 17 | factory TokenResponse([void Function(TokenResponseBuilder) updates]) = 18 | _$TokenResponse; 19 | 20 | factory TokenResponse.fromJson(Map json) => 21 | serializers.deserializeWith(serializer, json)!; 22 | 23 | Map toJson() => 24 | serializers.serializeWith(serializer, this) as Map; 25 | } 26 | -------------------------------------------------------------------------------- /lib/data/remote/response/token_response.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'token_response.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | Serializer _$tokenResponseSerializer = 10 | new _$TokenResponseSerializer(); 11 | 12 | class _$TokenResponseSerializer implements StructuredSerializer { 13 | @override 14 | final Iterable types = const [TokenResponse, _$TokenResponse]; 15 | @override 16 | final String wireName = 'TokenResponse'; 17 | 18 | @override 19 | Iterable serialize(Serializers serializers, TokenResponse object, 20 | {FullType specifiedType = FullType.unspecified}) { 21 | final result = [ 22 | 'message', 23 | serializers.serialize(object.message, 24 | specifiedType: const FullType(String)), 25 | ]; 26 | Object? value; 27 | value = object.token; 28 | if (value != null) { 29 | result 30 | ..add('token') 31 | ..add(serializers.serialize(value, 32 | specifiedType: const FullType(String))); 33 | } 34 | return result; 35 | } 36 | 37 | @override 38 | TokenResponse deserialize( 39 | Serializers serializers, Iterable serialized, 40 | {FullType specifiedType = FullType.unspecified}) { 41 | final result = new TokenResponseBuilder(); 42 | 43 | final iterator = serialized.iterator; 44 | while (iterator.moveNext()) { 45 | final key = iterator.current! as String; 46 | iterator.moveNext(); 47 | final Object? value = iterator.current; 48 | switch (key) { 49 | case 'token': 50 | result.token = serializers.deserialize(value, 51 | specifiedType: const FullType(String)) as String?; 52 | break; 53 | case 'message': 54 | result.message = serializers.deserialize(value, 55 | specifiedType: const FullType(String))! as String; 56 | break; 57 | } 58 | } 59 | 60 | return result.build(); 61 | } 62 | } 63 | 64 | class _$TokenResponse extends TokenResponse { 65 | @override 66 | final String? token; 67 | @override 68 | final String message; 69 | 70 | factory _$TokenResponse([void Function(TokenResponseBuilder)? updates]) => 71 | (new TokenResponseBuilder()..update(updates))._build(); 72 | 73 | _$TokenResponse._({this.token, required this.message}) : super._() { 74 | BuiltValueNullFieldError.checkNotNull(message, r'TokenResponse', 'message'); 75 | } 76 | 77 | @override 78 | TokenResponse rebuild(void Function(TokenResponseBuilder) updates) => 79 | (toBuilder()..update(updates)).build(); 80 | 81 | @override 82 | TokenResponseBuilder toBuilder() => new TokenResponseBuilder()..replace(this); 83 | 84 | @override 85 | bool operator ==(Object other) { 86 | if (identical(other, this)) return true; 87 | return other is TokenResponse && 88 | token == other.token && 89 | message == other.message; 90 | } 91 | 92 | @override 93 | int get hashCode { 94 | var _$hash = 0; 95 | _$hash = $jc(_$hash, token.hashCode); 96 | _$hash = $jc(_$hash, message.hashCode); 97 | _$hash = $jf(_$hash); 98 | return _$hash; 99 | } 100 | 101 | @override 102 | String toString() { 103 | return (newBuiltValueToStringHelper(r'TokenResponse') 104 | ..add('token', token) 105 | ..add('message', message)) 106 | .toString(); 107 | } 108 | } 109 | 110 | class TokenResponseBuilder 111 | implements Builder { 112 | _$TokenResponse? _$v; 113 | 114 | String? _token; 115 | String? get token => _$this._token; 116 | set token(String? token) => _$this._token = token; 117 | 118 | String? _message; 119 | String? get message => _$this._message; 120 | set message(String? message) => _$this._message = message; 121 | 122 | TokenResponseBuilder(); 123 | 124 | TokenResponseBuilder get _$this { 125 | final $v = _$v; 126 | if ($v != null) { 127 | _token = $v.token; 128 | _message = $v.message; 129 | _$v = null; 130 | } 131 | return this; 132 | } 133 | 134 | @override 135 | void replace(TokenResponse other) { 136 | ArgumentError.checkNotNull(other, 'other'); 137 | _$v = other as _$TokenResponse; 138 | } 139 | 140 | @override 141 | void update(void Function(TokenResponseBuilder)? updates) { 142 | if (updates != null) updates(this); 143 | } 144 | 145 | @override 146 | TokenResponse build() => _build(); 147 | 148 | _$TokenResponse _build() { 149 | final _$result = _$v ?? 150 | new _$TokenResponse._( 151 | token: token, 152 | message: BuiltValueNullFieldError.checkNotNull( 153 | message, r'TokenResponse', 'message')); 154 | replace(_$result); 155 | return _$result; 156 | } 157 | } 158 | 159 | // ignore_for_file: deprecated_member_use_from_same_package,type=lint 160 | -------------------------------------------------------------------------------- /lib/data/remote/response/user_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:node_auth/data/serializers.dart'; 4 | 5 | part 'user_response.g.dart'; 6 | 7 | abstract class UserResponse 8 | implements Built { 9 | @BuiltValueField(wireName: 'name') 10 | String get name; 11 | 12 | @BuiltValueField(wireName: 'email') 13 | String get email; 14 | 15 | @BuiltValueField(wireName: 'created_at') 16 | DateTime get createdAt; 17 | 18 | @BuiltValueField(wireName: 'image_url') 19 | String? get imageUrl; 20 | 21 | static Serializer get serializer => _$userResponseSerializer; 22 | 23 | UserResponse._(); 24 | 25 | factory UserResponse([void Function(UserResponseBuilder) updates]) = 26 | _$UserResponse; 27 | 28 | factory UserResponse.fromJson(Map json) => 29 | serializers.deserializeWith(serializer, json)!; 30 | 31 | Map toJson() => 32 | serializers.serializeWith(serializer, this) as Map; 33 | } 34 | -------------------------------------------------------------------------------- /lib/data/serializers.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/iso_8601_date_time_serializer.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:built_value/standard_json_plugin.dart'; 4 | import 'package:node_auth/data/local/entities/user_and_token_entity.dart'; 5 | import 'package:node_auth/data/local/entities/user_entity.dart'; 6 | import 'package:node_auth/data/remote/response/token_response.dart'; 7 | import 'package:node_auth/data/remote/response/user_response.dart'; 8 | 9 | part 'serializers.g.dart'; 10 | 11 | @SerializersFor([ 12 | UserEntity, 13 | UserAndTokenEntity, 14 | UserResponse, 15 | TokenResponse, 16 | ]) 17 | final Serializers serializers = (_$serializers.toBuilder() 18 | ..add(Iso8601DateTimeSerializer()) 19 | ..addPlugin(StandardJsonPlugin())) 20 | .build(); 21 | -------------------------------------------------------------------------------- /lib/data/serializers.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'serializers.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | Serializers _$serializers = (new Serializers().toBuilder() 10 | ..add(TokenResponse.serializer) 11 | ..add(UserAndTokenEntity.serializer) 12 | ..add(UserEntity.serializer) 13 | ..add(UserResponse.serializer)) 14 | .build(); 15 | 16 | // ignore_for_file: deprecated_member_use_from_same_package,type=lint 17 | -------------------------------------------------------------------------------- /lib/domain/models/app_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_either/dart_either.dart'; 2 | import 'package:http_client_hoc081098/http_client_hoc081098.dart'; 3 | import 'package:meta/meta.dart'; 4 | import 'package:node_auth/utils/unit.dart'; 5 | import 'package:rxdart_ext/rxdart_ext.dart'; 6 | 7 | export 'package:dart_either/dart_either.dart'; 8 | export 'package:rxdart_ext/rxdart_ext.dart'; 9 | 10 | @sealed 11 | class AppError { 12 | final String? _message; 13 | final Object? _error; 14 | final StackTrace? _stackTrace; 15 | 16 | /// Message of error 17 | /// Returns null if [isCancellation] is true 18 | String? get message => isCancellation ? null : _message; 19 | 20 | /// Get caused error 21 | /// Returns null if [isCancellation] is true 22 | Object? get error => isCancellation ? null : _error; 23 | 24 | /// Get stack trace 25 | /// Returns null if [isCancellation] is true 26 | StackTrace? get stackTrace => isCancellation ? null : _stackTrace; 27 | 28 | /// Returns true if this error is caused by cancellation 29 | bool get isCancellation => this is AppCancellationError; 30 | 31 | const AppError._( 32 | this._message, 33 | this._error, 34 | this._stackTrace, 35 | ); 36 | 37 | factory AppError({ 38 | required String message, 39 | required Object error, 40 | required StackTrace stackTrace, 41 | }) { 42 | if (error is CancellationException) { 43 | return const AppCancellationError(); 44 | } 45 | 46 | return AppError._( 47 | message, 48 | error, 49 | stackTrace, 50 | ); 51 | } 52 | 53 | @override 54 | String toString() => 55 | 'AppError{message: $_message, error: $_error, stackTrace: $_stackTrace}'; 56 | } 57 | 58 | class AppCancellationError extends AppError { 59 | const AppCancellationError() : super._(null, null, null); 60 | 61 | @override 62 | String toString() => 'AppCancellationError'; 63 | } 64 | 65 | typedef Result = Either; 66 | typedef UnitResult = Result; 67 | typedef UnitResultSingle = Single; 68 | -------------------------------------------------------------------------------- /lib/domain/models/auth_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:node_auth/domain/models/user_and_token.dart'; 4 | 5 | part 'auth_state.g.dart'; 6 | 7 | @immutable 8 | abstract class AuthenticationState { 9 | const AuthenticationState(); 10 | 11 | UserAndToken? get userAndToken; 12 | } 13 | 14 | abstract class AuthenticatedState 15 | implements 16 | Built, 17 | AuthenticationState { 18 | @override 19 | UserAndToken get userAndToken; 20 | 21 | AuthenticatedState._(); 22 | 23 | factory AuthenticatedState( 24 | [void Function(AuthenticatedStateBuilder) updates]) = 25 | _$AuthenticatedState; 26 | } 27 | 28 | abstract class UnauthenticatedState 29 | implements 30 | Built, 31 | AuthenticationState { 32 | @override 33 | UserAndToken? get userAndToken => null; 34 | 35 | UnauthenticatedState._(); 36 | 37 | factory UnauthenticatedState( 38 | [void Function(UnauthenticatedStateBuilder) updates]) = 39 | _$UnauthenticatedState; 40 | } 41 | -------------------------------------------------------------------------------- /lib/domain/models/auth_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'auth_state.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | class _$AuthenticatedState extends AuthenticatedState { 10 | @override 11 | final UserAndToken userAndToken; 12 | 13 | factory _$AuthenticatedState( 14 | [void Function(AuthenticatedStateBuilder)? updates]) => 15 | (new AuthenticatedStateBuilder()..update(updates))._build(); 16 | 17 | _$AuthenticatedState._({required this.userAndToken}) : super._() { 18 | BuiltValueNullFieldError.checkNotNull( 19 | userAndToken, r'AuthenticatedState', 'userAndToken'); 20 | } 21 | 22 | @override 23 | AuthenticatedState rebuild( 24 | void Function(AuthenticatedStateBuilder) updates) => 25 | (toBuilder()..update(updates)).build(); 26 | 27 | @override 28 | AuthenticatedStateBuilder toBuilder() => 29 | new AuthenticatedStateBuilder()..replace(this); 30 | 31 | @override 32 | bool operator ==(Object other) { 33 | if (identical(other, this)) return true; 34 | return other is AuthenticatedState && userAndToken == other.userAndToken; 35 | } 36 | 37 | @override 38 | int get hashCode { 39 | var _$hash = 0; 40 | _$hash = $jc(_$hash, userAndToken.hashCode); 41 | _$hash = $jf(_$hash); 42 | return _$hash; 43 | } 44 | 45 | @override 46 | String toString() { 47 | return (newBuiltValueToStringHelper(r'AuthenticatedState') 48 | ..add('userAndToken', userAndToken)) 49 | .toString(); 50 | } 51 | } 52 | 53 | class AuthenticatedStateBuilder 54 | implements Builder { 55 | _$AuthenticatedState? _$v; 56 | 57 | UserAndTokenBuilder? _userAndToken; 58 | UserAndTokenBuilder get userAndToken => 59 | _$this._userAndToken ??= new UserAndTokenBuilder(); 60 | set userAndToken(UserAndTokenBuilder? userAndToken) => 61 | _$this._userAndToken = userAndToken; 62 | 63 | AuthenticatedStateBuilder(); 64 | 65 | AuthenticatedStateBuilder get _$this { 66 | final $v = _$v; 67 | if ($v != null) { 68 | _userAndToken = $v.userAndToken.toBuilder(); 69 | _$v = null; 70 | } 71 | return this; 72 | } 73 | 74 | @override 75 | void replace(AuthenticatedState other) { 76 | ArgumentError.checkNotNull(other, 'other'); 77 | _$v = other as _$AuthenticatedState; 78 | } 79 | 80 | @override 81 | void update(void Function(AuthenticatedStateBuilder)? updates) { 82 | if (updates != null) updates(this); 83 | } 84 | 85 | @override 86 | AuthenticatedState build() => _build(); 87 | 88 | _$AuthenticatedState _build() { 89 | _$AuthenticatedState _$result; 90 | try { 91 | _$result = 92 | _$v ?? new _$AuthenticatedState._(userAndToken: userAndToken.build()); 93 | } catch (_) { 94 | late String _$failedField; 95 | try { 96 | _$failedField = 'userAndToken'; 97 | userAndToken.build(); 98 | } catch (e) { 99 | throw new BuiltValueNestedFieldError( 100 | r'AuthenticatedState', _$failedField, e.toString()); 101 | } 102 | rethrow; 103 | } 104 | replace(_$result); 105 | return _$result; 106 | } 107 | } 108 | 109 | class _$UnauthenticatedState extends UnauthenticatedState { 110 | factory _$UnauthenticatedState( 111 | [void Function(UnauthenticatedStateBuilder)? updates]) => 112 | (new UnauthenticatedStateBuilder()..update(updates))._build(); 113 | 114 | _$UnauthenticatedState._() : super._(); 115 | 116 | @override 117 | UnauthenticatedState rebuild( 118 | void Function(UnauthenticatedStateBuilder) updates) => 119 | (toBuilder()..update(updates)).build(); 120 | 121 | @override 122 | UnauthenticatedStateBuilder toBuilder() => 123 | new UnauthenticatedStateBuilder()..replace(this); 124 | 125 | @override 126 | bool operator ==(Object other) { 127 | if (identical(other, this)) return true; 128 | return other is UnauthenticatedState; 129 | } 130 | 131 | @override 132 | int get hashCode { 133 | return 228826372; 134 | } 135 | 136 | @override 137 | String toString() { 138 | return newBuiltValueToStringHelper(r'UnauthenticatedState').toString(); 139 | } 140 | } 141 | 142 | class UnauthenticatedStateBuilder 143 | implements Builder { 144 | _$UnauthenticatedState? _$v; 145 | 146 | UnauthenticatedStateBuilder(); 147 | 148 | @override 149 | void replace(UnauthenticatedState other) { 150 | ArgumentError.checkNotNull(other, 'other'); 151 | _$v = other as _$UnauthenticatedState; 152 | } 153 | 154 | @override 155 | void update(void Function(UnauthenticatedStateBuilder)? updates) { 156 | if (updates != null) updates(this); 157 | } 158 | 159 | @override 160 | UnauthenticatedState build() => _build(); 161 | 162 | _$UnauthenticatedState _build() { 163 | final _$result = _$v ?? new _$UnauthenticatedState._(); 164 | replace(_$result); 165 | return _$result; 166 | } 167 | } 168 | 169 | // ignore_for_file: deprecated_member_use_from_same_package,type=lint 170 | -------------------------------------------------------------------------------- /lib/domain/models/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | 3 | part 'user.g.dart'; 4 | 5 | abstract class User implements Built { 6 | String get name; 7 | 8 | String get email; 9 | 10 | DateTime get createdAt; 11 | 12 | String? get imageUrl; 13 | 14 | User._(); 15 | 16 | factory User([void Function(UserBuilder) updates]) = _$User; 17 | } 18 | -------------------------------------------------------------------------------- /lib/domain/models/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | class _$User extends User { 10 | @override 11 | final String name; 12 | @override 13 | final String email; 14 | @override 15 | final DateTime createdAt; 16 | @override 17 | final String? imageUrl; 18 | 19 | factory _$User([void Function(UserBuilder)? updates]) => 20 | (new UserBuilder()..update(updates))._build(); 21 | 22 | _$User._( 23 | {required this.name, 24 | required this.email, 25 | required this.createdAt, 26 | this.imageUrl}) 27 | : super._() { 28 | BuiltValueNullFieldError.checkNotNull(name, r'User', 'name'); 29 | BuiltValueNullFieldError.checkNotNull(email, r'User', 'email'); 30 | BuiltValueNullFieldError.checkNotNull(createdAt, r'User', 'createdAt'); 31 | } 32 | 33 | @override 34 | User rebuild(void Function(UserBuilder) updates) => 35 | (toBuilder()..update(updates)).build(); 36 | 37 | @override 38 | UserBuilder toBuilder() => new UserBuilder()..replace(this); 39 | 40 | @override 41 | bool operator ==(Object other) { 42 | if (identical(other, this)) return true; 43 | return other is User && 44 | name == other.name && 45 | email == other.email && 46 | createdAt == other.createdAt && 47 | imageUrl == other.imageUrl; 48 | } 49 | 50 | @override 51 | int get hashCode { 52 | var _$hash = 0; 53 | _$hash = $jc(_$hash, name.hashCode); 54 | _$hash = $jc(_$hash, email.hashCode); 55 | _$hash = $jc(_$hash, createdAt.hashCode); 56 | _$hash = $jc(_$hash, imageUrl.hashCode); 57 | _$hash = $jf(_$hash); 58 | return _$hash; 59 | } 60 | 61 | @override 62 | String toString() { 63 | return (newBuiltValueToStringHelper(r'User') 64 | ..add('name', name) 65 | ..add('email', email) 66 | ..add('createdAt', createdAt) 67 | ..add('imageUrl', imageUrl)) 68 | .toString(); 69 | } 70 | } 71 | 72 | class UserBuilder implements Builder { 73 | _$User? _$v; 74 | 75 | String? _name; 76 | String? get name => _$this._name; 77 | set name(String? name) => _$this._name = name; 78 | 79 | String? _email; 80 | String? get email => _$this._email; 81 | set email(String? email) => _$this._email = email; 82 | 83 | DateTime? _createdAt; 84 | DateTime? get createdAt => _$this._createdAt; 85 | set createdAt(DateTime? createdAt) => _$this._createdAt = createdAt; 86 | 87 | String? _imageUrl; 88 | String? get imageUrl => _$this._imageUrl; 89 | set imageUrl(String? imageUrl) => _$this._imageUrl = imageUrl; 90 | 91 | UserBuilder(); 92 | 93 | UserBuilder get _$this { 94 | final $v = _$v; 95 | if ($v != null) { 96 | _name = $v.name; 97 | _email = $v.email; 98 | _createdAt = $v.createdAt; 99 | _imageUrl = $v.imageUrl; 100 | _$v = null; 101 | } 102 | return this; 103 | } 104 | 105 | @override 106 | void replace(User other) { 107 | ArgumentError.checkNotNull(other, 'other'); 108 | _$v = other as _$User; 109 | } 110 | 111 | @override 112 | void update(void Function(UserBuilder)? updates) { 113 | if (updates != null) updates(this); 114 | } 115 | 116 | @override 117 | User build() => _build(); 118 | 119 | _$User _build() { 120 | final _$result = _$v ?? 121 | new _$User._( 122 | name: BuiltValueNullFieldError.checkNotNull(name, r'User', 'name'), 123 | email: 124 | BuiltValueNullFieldError.checkNotNull(email, r'User', 'email'), 125 | createdAt: BuiltValueNullFieldError.checkNotNull( 126 | createdAt, r'User', 'createdAt'), 127 | imageUrl: imageUrl); 128 | replace(_$result); 129 | return _$result; 130 | } 131 | } 132 | 133 | // ignore_for_file: deprecated_member_use_from_same_package,type=lint 134 | -------------------------------------------------------------------------------- /lib/domain/models/user_and_token.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:node_auth/domain/models/user.dart'; 3 | 4 | part 'user_and_token.g.dart'; 5 | 6 | abstract class UserAndToken 7 | implements Built { 8 | String get token; 9 | 10 | User get user; 11 | 12 | UserAndToken._(); 13 | 14 | factory UserAndToken([void Function(UserAndTokenBuilder) updates]) = 15 | _$UserAndToken; 16 | } 17 | -------------------------------------------------------------------------------- /lib/domain/models/user_and_token.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user_and_token.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | class _$UserAndToken extends UserAndToken { 10 | @override 11 | final String token; 12 | @override 13 | final User user; 14 | 15 | factory _$UserAndToken([void Function(UserAndTokenBuilder)? updates]) => 16 | (new UserAndTokenBuilder()..update(updates))._build(); 17 | 18 | _$UserAndToken._({required this.token, required this.user}) : super._() { 19 | BuiltValueNullFieldError.checkNotNull(token, r'UserAndToken', 'token'); 20 | BuiltValueNullFieldError.checkNotNull(user, r'UserAndToken', 'user'); 21 | } 22 | 23 | @override 24 | UserAndToken rebuild(void Function(UserAndTokenBuilder) updates) => 25 | (toBuilder()..update(updates)).build(); 26 | 27 | @override 28 | UserAndTokenBuilder toBuilder() => new UserAndTokenBuilder()..replace(this); 29 | 30 | @override 31 | bool operator ==(Object other) { 32 | if (identical(other, this)) return true; 33 | return other is UserAndToken && token == other.token && user == other.user; 34 | } 35 | 36 | @override 37 | int get hashCode { 38 | var _$hash = 0; 39 | _$hash = $jc(_$hash, token.hashCode); 40 | _$hash = $jc(_$hash, user.hashCode); 41 | _$hash = $jf(_$hash); 42 | return _$hash; 43 | } 44 | 45 | @override 46 | String toString() { 47 | return (newBuiltValueToStringHelper(r'UserAndToken') 48 | ..add('token', token) 49 | ..add('user', user)) 50 | .toString(); 51 | } 52 | } 53 | 54 | class UserAndTokenBuilder 55 | implements Builder { 56 | _$UserAndToken? _$v; 57 | 58 | String? _token; 59 | String? get token => _$this._token; 60 | set token(String? token) => _$this._token = token; 61 | 62 | UserBuilder? _user; 63 | UserBuilder get user => _$this._user ??= new UserBuilder(); 64 | set user(UserBuilder? user) => _$this._user = user; 65 | 66 | UserAndTokenBuilder(); 67 | 68 | UserAndTokenBuilder get _$this { 69 | final $v = _$v; 70 | if ($v != null) { 71 | _token = $v.token; 72 | _user = $v.user.toBuilder(); 73 | _$v = null; 74 | } 75 | return this; 76 | } 77 | 78 | @override 79 | void replace(UserAndToken other) { 80 | ArgumentError.checkNotNull(other, 'other'); 81 | _$v = other as _$UserAndToken; 82 | } 83 | 84 | @override 85 | void update(void Function(UserAndTokenBuilder)? updates) { 86 | if (updates != null) updates(this); 87 | } 88 | 89 | @override 90 | UserAndToken build() => _build(); 91 | 92 | _$UserAndToken _build() { 93 | _$UserAndToken _$result; 94 | try { 95 | _$result = _$v ?? 96 | new _$UserAndToken._( 97 | token: BuiltValueNullFieldError.checkNotNull( 98 | token, r'UserAndToken', 'token'), 99 | user: user.build()); 100 | } catch (_) { 101 | late String _$failedField; 102 | try { 103 | _$failedField = 'user'; 104 | user.build(); 105 | } catch (e) { 106 | throw new BuiltValueNestedFieldError( 107 | r'UserAndToken', _$failedField, e.toString()); 108 | } 109 | rethrow; 110 | } 111 | replace(_$result); 112 | return _$result; 113 | } 114 | } 115 | 116 | // ignore_for_file: deprecated_member_use_from_same_package,type=lint 117 | -------------------------------------------------------------------------------- /lib/domain/repositories/user_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:node_auth/domain/models/app_error.dart'; 4 | import 'package:node_auth/domain/models/auth_state.dart'; 5 | 6 | abstract class UserRepository { 7 | Stream> get authenticationState$; 8 | 9 | Single> get authenticationState; 10 | 11 | UnitResultSingle login({ 12 | required String email, 13 | required String password, 14 | }); 15 | 16 | UnitResultSingle registerUser({ 17 | required String name, 18 | required String email, 19 | required String password, 20 | }); 21 | 22 | UnitResultSingle logout(); 23 | 24 | UnitResultSingle uploadImage(File image); 25 | 26 | UnitResultSingle changePassword({ 27 | required String password, 28 | required String newPassword, 29 | }); 30 | 31 | UnitResultSingle resetPassword({ 32 | required String email, 33 | required String token, 34 | required String newPassword, 35 | }); 36 | 37 | UnitResultSingle sendResetPasswordEmail(String email); 38 | } 39 | -------------------------------------------------------------------------------- /lib/domain/usecases/change_password_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:node_auth/domain/models/app_error.dart'; 2 | import 'package:node_auth/domain/repositories/user_repository.dart'; 3 | 4 | class ChangePasswordUseCase { 5 | final UserRepository _userRepository; 6 | 7 | const ChangePasswordUseCase(this._userRepository); 8 | 9 | UnitResultSingle call({ 10 | required String password, 11 | required String newPassword, 12 | }) => 13 | _userRepository.changePassword( 14 | password: password, 15 | newPassword: newPassword, 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /lib/domain/usecases/get_auth_state_stream_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:node_auth/domain/models/app_error.dart'; 2 | import 'package:node_auth/domain/models/auth_state.dart'; 3 | import 'package:node_auth/domain/repositories/user_repository.dart'; 4 | 5 | class GetAuthStateStreamUseCase { 6 | final UserRepository _userRepository; 7 | 8 | const GetAuthStateStreamUseCase(this._userRepository); 9 | 10 | Stream> call() => 11 | _userRepository.authenticationState$; 12 | } 13 | -------------------------------------------------------------------------------- /lib/domain/usecases/get_auth_state_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:node_auth/domain/models/app_error.dart'; 2 | import 'package:node_auth/domain/models/auth_state.dart'; 3 | import 'package:node_auth/domain/repositories/user_repository.dart'; 4 | 5 | class GetAuthStateUseCase { 6 | final UserRepository _userRepository; 7 | 8 | const GetAuthStateUseCase(this._userRepository); 9 | 10 | Single> call() => 11 | _userRepository.authenticationState; 12 | } 13 | -------------------------------------------------------------------------------- /lib/domain/usecases/login_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:node_auth/domain/models/app_error.dart'; 2 | import 'package:node_auth/domain/repositories/user_repository.dart'; 3 | 4 | class LoginUseCase { 5 | final UserRepository _userRepository; 6 | 7 | const LoginUseCase(this._userRepository); 8 | 9 | UnitResultSingle call({ 10 | required String email, 11 | required String password, 12 | }) => 13 | _userRepository.login(email: email, password: password); 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/usecases/logout_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:node_auth/domain/models/app_error.dart'; 2 | import 'package:node_auth/domain/repositories/user_repository.dart'; 3 | 4 | class LogoutUseCase { 5 | final UserRepository _userRepository; 6 | 7 | const LogoutUseCase(this._userRepository); 8 | 9 | UnitResultSingle call() => _userRepository.logout(); 10 | } 11 | -------------------------------------------------------------------------------- /lib/domain/usecases/register_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:node_auth/domain/models/app_error.dart'; 2 | import 'package:node_auth/domain/repositories/user_repository.dart'; 3 | 4 | class RegisterUseCase { 5 | final UserRepository _userRepository; 6 | 7 | const RegisterUseCase(this._userRepository); 8 | 9 | UnitResultSingle call({ 10 | required String name, 11 | required String email, 12 | required String password, 13 | }) => 14 | _userRepository.registerUser( 15 | name: name, 16 | email: email, 17 | password: password, 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /lib/domain/usecases/reset_password_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:node_auth/domain/models/app_error.dart'; 2 | import 'package:node_auth/domain/repositories/user_repository.dart'; 3 | 4 | class ResetPasswordUseCase { 5 | final UserRepository _userRepository; 6 | 7 | const ResetPasswordUseCase(this._userRepository); 8 | 9 | UnitResultSingle call({ 10 | required String email, 11 | required String token, 12 | required String newPassword, 13 | }) => 14 | _userRepository.resetPassword( 15 | email: email, 16 | token: token, 17 | newPassword: newPassword, 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /lib/domain/usecases/send_reset_password_email_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:node_auth/domain/models/app_error.dart'; 2 | import 'package:node_auth/domain/repositories/user_repository.dart'; 3 | 4 | class SendResetPasswordEmailUseCase { 5 | final UserRepository _userRepository; 6 | 7 | const SendResetPasswordEmailUseCase(this._userRepository); 8 | 9 | UnitResultSingle call(String email) => 10 | _userRepository.sendResetPasswordEmail(email); 11 | } 12 | -------------------------------------------------------------------------------- /lib/domain/usecases/upload_image_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:node_auth/domain/models/app_error.dart'; 4 | import 'package:node_auth/domain/repositories/user_repository.dart'; 5 | 6 | class UploadImageUseCase { 7 | final UserRepository _userRepository; 8 | 9 | const UploadImageUseCase(this._userRepository); 10 | 11 | UnitResultSingle call(File image) => _userRepository.uploadImage(image); 12 | } 13 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:cupertino_http/cupertino_http.dart'; 4 | import 'package:disposebag/disposebag.dart'; 5 | import 'package:flutter/foundation.dart' 6 | show debugPrint, debugPrintSynchronously, kReleaseMode; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter/services.dart'; 9 | import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart'; 10 | import 'package:flutter_provider/flutter_provider.dart'; 11 | import 'package:http/http.dart' as http; 12 | import 'package:http_client_hoc081098/http_client_hoc081098.dart'; 13 | import 'package:node_auth/app.dart'; 14 | import 'package:node_auth/data/local/local_data_source.dart'; 15 | import 'package:node_auth/data/local/method_channel_crypto_impl.dart'; 16 | import 'package:node_auth/data/local/shared_pref_util.dart'; 17 | import 'package:node_auth/data/remote/api_service.dart'; 18 | import 'package:node_auth/data/remote/auth_interceptor.dart'; 19 | import 'package:node_auth/data/remote/remote_data_source.dart'; 20 | import 'package:node_auth/data/user_repository_imp.dart'; 21 | import 'package:node_auth/domain/repositories/user_repository.dart'; 22 | import 'package:rx_shared_preferences/rx_shared_preferences.dart'; 23 | 24 | void main() async { 25 | WidgetsFlutterBinding.ensureInitialized(); 26 | RxStreamBuilder.checkStateStreamEnabled = !kReleaseMode; 27 | _setupLoggers(); 28 | await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); 29 | 30 | // Create http client 31 | late final Func0> onUnauthorized; 32 | final simpleHttpClient = createSimpleHttpClient(() => onUnauthorized()); 33 | 34 | // construct RemoteDataSource 35 | final RemoteDataSource remoteDataSource = ApiService(simpleHttpClient); 36 | 37 | // construct LocalDataSource 38 | final rxPrefs = RxSharedPreferences.getInstance(); 39 | final crypto = MethodChannelCryptoImpl(); 40 | final LocalDataSource localDataSource = SharedPrefUtil(rxPrefs, crypto); 41 | onUnauthorized = () => localDataSource.removeUserAndToken().first; 42 | 43 | // construct UserRepository 44 | final UserRepository userRepository = UserRepositoryImpl( 45 | remoteDataSource, 46 | localDataSource, 47 | ); 48 | 49 | runApp( 50 | Provider.value( 51 | userRepository, 52 | child: const MyApp(), 53 | ), 54 | ); 55 | } 56 | 57 | SimpleHttpClient createSimpleHttpClient( 58 | Func0> onUnauthorized, 59 | ) { 60 | final authInterceptor = AuthInterceptor(onUnauthorized: onUnauthorized); 61 | 62 | final loggingInterceptor = SimpleLoggingInterceptor( 63 | SimpleLogger( 64 | loggerFunction: print, 65 | level: kReleaseMode ? SimpleLogLevel.none : SimpleLogLevel.body, 66 | headersToRedact: { 67 | ApiService.xAccessToken, 68 | HttpHeaders.authorizationHeader, 69 | }, 70 | ), 71 | ); 72 | 73 | final simpleHttpClient = SimpleHttpClient( 74 | client: Platform.isIOS || Platform.isMacOS 75 | ? CupertinoClient.defaultSessionConfiguration() 76 | : http.Client(), 77 | timeout: const Duration(seconds: 20), 78 | requestInterceptors: [ 79 | authInterceptor.requestInterceptor, 80 | // others interceptors above this line 81 | loggingInterceptor.requestInterceptor, 82 | ], 83 | responseInterceptors: [ 84 | loggingInterceptor.responseInterceptor, 85 | // others interceptors below this line 86 | authInterceptor.responseInterceptor, 87 | ], 88 | ); 89 | 90 | return simpleHttpClient; 91 | } 92 | 93 | void _setupLoggers() { 94 | // set loggers to `null` to disable logging. 95 | DisposeBagConfigs.logger = kReleaseMode ? null : disposeBagDefaultLogger; 96 | 97 | RxSharedPreferencesConfigs.logger = 98 | kReleaseMode ? null : const RxSharedPreferencesDefaultLogger(); 99 | 100 | debugPrint = kReleaseMode 101 | ? (String? message, {int? wrapWidth}) {} 102 | : debugPrintSynchronously; 103 | } 104 | -------------------------------------------------------------------------------- /lib/pages/home/change_password/change_password.dart: -------------------------------------------------------------------------------- 1 | export 'change_password_bloc.dart'; 2 | export 'change_password_bottomsheet.dart'; 3 | export 'change_password_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/pages/home/change_password/change_password_bottomsheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart'; 3 | import 'package:flutter_disposebag/flutter_disposebag.dart'; 4 | import 'package:node_auth/pages/home/change_password/change_password.dart'; 5 | import 'package:node_auth/widgets/password_textfield.dart'; 6 | import 'package:rxdart_ext/rxdart_ext.dart'; 7 | 8 | class ChangePasswordBottomSheet extends StatefulWidget { 9 | const ChangePasswordBottomSheet({super.key}); 10 | 11 | @override 12 | State createState() => 13 | _ChangePasswordBottomSheetState(); 14 | } 15 | 16 | class _ChangePasswordBottomSheetState extends State 17 | with 18 | SingleTickerProviderStateMixin, 19 | DisposeBagMixin { 20 | late final AnimationController fadeMessageController; 21 | late final Animation messageOpacity; 22 | Object? listen; 23 | 24 | final newPasswordFocusNode = FocusNode(); 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | 30 | fadeMessageController = AnimationController( 31 | vsync: this, 32 | duration: const Duration(milliseconds: 1200), 33 | ); 34 | messageOpacity = Tween( 35 | begin: 1.0, 36 | end: 0.0, 37 | ).animate( 38 | CurvedAnimation( 39 | parent: fadeMessageController, 40 | curve: Curves.bounceIn, 41 | ), 42 | ); 43 | } 44 | 45 | @override 46 | void didChangeDependencies() { 47 | super.didChangeDependencies(); 48 | 49 | listen ??= BlocProvider.of(context) 50 | .changePasswordState$ 51 | .flatMap((state) async* { 52 | if (state.message != null) { 53 | fadeMessageController.reset(); 54 | await fadeMessageController.forward(); 55 | yield null; 56 | 57 | if (state.error == null) { 58 | // ignore: use_build_context_synchronously 59 | Navigator.of(context).pop(); 60 | } 61 | } 62 | }) 63 | .collect() 64 | .disposedBy(bag); 65 | } 66 | 67 | @override 68 | void dispose() { 69 | fadeMessageController.dispose(); 70 | newPasswordFocusNode.dispose(); 71 | super.dispose(); 72 | } 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | final changePasswordBloc = BlocProvider.of(context); 77 | 78 | final passwordTextField = StreamBuilder( 79 | stream: changePasswordBloc.passwordError$, 80 | builder: (context, snapshot) { 81 | return PasswordTextField( 82 | errorText: snapshot.data, 83 | onChanged: changePasswordBloc.passwordChanged, 84 | labelText: 'Old password', 85 | onSubmitted: () { 86 | FocusScope.of(context).requestFocus(newPasswordFocusNode); 87 | }, 88 | textInputAction: TextInputAction.next, 89 | focusNode: null, 90 | ); 91 | }, 92 | ); 93 | 94 | final newPasswordTextField = StreamBuilder( 95 | stream: changePasswordBloc.newPasswordError$, 96 | builder: (context, snapshot) { 97 | return PasswordTextField( 98 | errorText: snapshot.data, 99 | onChanged: changePasswordBloc.newPasswordChanged, 100 | labelText: 'New password', 101 | focusNode: newPasswordFocusNode, 102 | onSubmitted: () { 103 | FocusScope.of(context).unfocus(); 104 | }, 105 | textInputAction: TextInputAction.done, 106 | ); 107 | }, 108 | ); 109 | 110 | final messageText = RxStreamBuilder( 111 | stream: changePasswordBloc.changePasswordState$, 112 | builder: (context, state) { 113 | final message = state.message; 114 | if (message != null) { 115 | return Padding( 116 | padding: const EdgeInsets.all(8.0), 117 | child: FadeTransition( 118 | opacity: messageOpacity, 119 | child: Text( 120 | message, 121 | style: const TextStyle( 122 | fontSize: 14.0, 123 | fontStyle: FontStyle.italic, 124 | ), 125 | ), 126 | ), 127 | ); 128 | } 129 | return const SizedBox(width: 0, height: 0); 130 | }, 131 | ); 132 | 133 | final changePasswordButton = RxStreamBuilder( 134 | stream: changePasswordBloc.changePasswordState$, 135 | builder: (context, state) { 136 | if (!state.isLoading) { 137 | return ElevatedButton( 138 | style: ElevatedButton.styleFrom( 139 | padding: const EdgeInsets.all(12), 140 | elevation: 4, 141 | ), 142 | onPressed: () { 143 | FocusScope.of(context).unfocus(); 144 | changePasswordBloc.changePassword(); 145 | }, 146 | child: const Text( 147 | 'Change password', 148 | style: TextStyle(fontSize: 16.0), 149 | ), 150 | ); 151 | } 152 | return const CircularProgressIndicator(); 153 | }, 154 | ); 155 | 156 | return SingleChildScrollView( 157 | child: Column( 158 | mainAxisSize: MainAxisSize.min, 159 | mainAxisAlignment: MainAxisAlignment.start, 160 | children: [ 161 | Padding( 162 | padding: const EdgeInsets.all(8.0), 163 | child: passwordTextField, 164 | ), 165 | Padding( 166 | padding: const EdgeInsets.all(8.0), 167 | child: newPasswordTextField, 168 | ), 169 | messageText, 170 | Padding( 171 | padding: const EdgeInsets.all(32.0), 172 | child: changePasswordButton, 173 | ), 174 | SizedBox( 175 | height: MediaQuery.of(context).viewInsets.bottom, 176 | ) 177 | ], 178 | ), 179 | ); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /lib/pages/home/change_password/change_password_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | 3 | part 'change_password_state.g.dart'; 4 | 5 | abstract class ChangePasswordState 6 | implements Built { 7 | Object? get error; 8 | 9 | String? get message; 10 | 11 | bool get isLoading; 12 | 13 | ChangePasswordState._(); 14 | 15 | factory ChangePasswordState( 16 | [void Function(ChangePasswordStateBuilder) updates]) = 17 | _$ChangePasswordState; 18 | } 19 | -------------------------------------------------------------------------------- /lib/pages/home/change_password/change_password_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'change_password_state.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | class _$ChangePasswordState extends ChangePasswordState { 10 | @override 11 | final Object? error; 12 | @override 13 | final String? message; 14 | @override 15 | final bool isLoading; 16 | 17 | factory _$ChangePasswordState( 18 | [void Function(ChangePasswordStateBuilder)? updates]) => 19 | (new ChangePasswordStateBuilder()..update(updates))._build(); 20 | 21 | _$ChangePasswordState._({this.error, this.message, required this.isLoading}) 22 | : super._() { 23 | BuiltValueNullFieldError.checkNotNull( 24 | isLoading, r'ChangePasswordState', 'isLoading'); 25 | } 26 | 27 | @override 28 | ChangePasswordState rebuild( 29 | void Function(ChangePasswordStateBuilder) updates) => 30 | (toBuilder()..update(updates)).build(); 31 | 32 | @override 33 | ChangePasswordStateBuilder toBuilder() => 34 | new ChangePasswordStateBuilder()..replace(this); 35 | 36 | @override 37 | bool operator ==(Object other) { 38 | if (identical(other, this)) return true; 39 | return other is ChangePasswordState && 40 | error == other.error && 41 | message == other.message && 42 | isLoading == other.isLoading; 43 | } 44 | 45 | @override 46 | int get hashCode { 47 | var _$hash = 0; 48 | _$hash = $jc(_$hash, error.hashCode); 49 | _$hash = $jc(_$hash, message.hashCode); 50 | _$hash = $jc(_$hash, isLoading.hashCode); 51 | _$hash = $jf(_$hash); 52 | return _$hash; 53 | } 54 | 55 | @override 56 | String toString() { 57 | return (newBuiltValueToStringHelper(r'ChangePasswordState') 58 | ..add('error', error) 59 | ..add('message', message) 60 | ..add('isLoading', isLoading)) 61 | .toString(); 62 | } 63 | } 64 | 65 | class ChangePasswordStateBuilder 66 | implements Builder { 67 | _$ChangePasswordState? _$v; 68 | 69 | Object? _error; 70 | Object? get error => _$this._error; 71 | set error(Object? error) => _$this._error = error; 72 | 73 | String? _message; 74 | String? get message => _$this._message; 75 | set message(String? message) => _$this._message = message; 76 | 77 | bool? _isLoading; 78 | bool? get isLoading => _$this._isLoading; 79 | set isLoading(bool? isLoading) => _$this._isLoading = isLoading; 80 | 81 | ChangePasswordStateBuilder(); 82 | 83 | ChangePasswordStateBuilder get _$this { 84 | final $v = _$v; 85 | if ($v != null) { 86 | _error = $v.error; 87 | _message = $v.message; 88 | _isLoading = $v.isLoading; 89 | _$v = null; 90 | } 91 | return this; 92 | } 93 | 94 | @override 95 | void replace(ChangePasswordState other) { 96 | ArgumentError.checkNotNull(other, 'other'); 97 | _$v = other as _$ChangePasswordState; 98 | } 99 | 100 | @override 101 | void update(void Function(ChangePasswordStateBuilder)? updates) { 102 | if (updates != null) updates(this); 103 | } 104 | 105 | @override 106 | ChangePasswordState build() => _build(); 107 | 108 | _$ChangePasswordState _build() { 109 | final _$result = _$v ?? 110 | new _$ChangePasswordState._( 111 | error: error, 112 | message: message, 113 | isLoading: BuiltValueNullFieldError.checkNotNull( 114 | isLoading, r'ChangePasswordState', 'isLoading')); 115 | replace(_$result); 116 | return _$result; 117 | } 118 | } 119 | 120 | // ignore_for_file: deprecated_member_use_from_same_package,type=lint 121 | -------------------------------------------------------------------------------- /lib/pages/home/home.dart: -------------------------------------------------------------------------------- 1 | export 'home_bloc.dart'; 2 | export 'home_page.dart'; 3 | export 'home_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/pages/home/home_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:disposebag/disposebag.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart'; 7 | import 'package:image_picker/image_picker.dart'; 8 | import 'package:node_auth/domain/models/app_error.dart'; 9 | import 'package:node_auth/domain/models/auth_state.dart'; 10 | import 'package:node_auth/domain/usecases/get_auth_state_stream_use_case.dart'; 11 | import 'package:node_auth/domain/usecases/logout_use_case.dart'; 12 | import 'package:node_auth/domain/usecases/upload_image_use_case.dart'; 13 | import 'package:node_auth/pages/home/home_state.dart'; 14 | import 'package:node_auth/utils/streams.dart'; 15 | import 'package:node_auth/utils/type_defs.dart'; 16 | 17 | //ignore_for_file: close_sinks 18 | 19 | /// BLoC that handles user profile and logout 20 | class HomeBloc extends DisposeCallbackBaseBloc { 21 | /// Input functions 22 | final Function0 changeAvatar; 23 | final Function0 logout; 24 | 25 | /// Output stream 26 | final StateStream?> authState$; 27 | final Stream message$; 28 | final StateStream isUploading$; 29 | 30 | HomeBloc._({ 31 | required this.changeAvatar, 32 | required this.message$, 33 | required this.logout, 34 | required this.authState$, 35 | required this.isUploading$, 36 | required Function0 dispose, 37 | }) : super(dispose); 38 | 39 | factory HomeBloc( 40 | final LogoutUseCase logout, 41 | final GetAuthStateStreamUseCase getAuthState, 42 | final UploadImageUseCase uploadImage, 43 | ) { 44 | final changeAvatarS = PublishSubject(); 45 | final logoutS = PublishSubject(); 46 | final isUploading$ = StateSubject(false); 47 | 48 | final authenticationState$ = getAuthState(); 49 | 50 | final logoutMessage$ = Rx.merge([ 51 | logoutS.exhaustMap((_) => logout()).map(_resultToLogoutMessage), 52 | authenticationState$ 53 | .where((result) => result.orNull()?.userAndToken == null) 54 | .mapTo(const LogoutSuccessMessage()), 55 | ]); 56 | 57 | final imagePicker = ImagePicker(); 58 | final updateAvatarMessage$ = changeAvatarS 59 | .debug(identifier: 'changeAvatar [1]', log: debugPrint) 60 | .switchMap( 61 | (_) => Rx.fromCallable( 62 | () => imagePicker.pickImage( 63 | source: ImageSource.gallery, 64 | maxWidth: 720.0, 65 | maxHeight: 720.0, 66 | ), 67 | ).debug(identifier: 'pickImage', log: debugPrint), 68 | ) 69 | .debug(identifier: 'changeAvatar [2]', log: debugPrint) 70 | .map((file) => file == null ? null : File(file.path)) 71 | .whereNotNull() 72 | .distinct() 73 | .switchMap( 74 | (file) => uploadImage(file).doOn( 75 | listen: () => isUploading$.value = true, 76 | cancel: () => isUploading$.value = false), 77 | ) 78 | .debug(identifier: 'changeAvatar [3]', log: debugPrint) 79 | .map(_resultToChangeAvatarMessage); 80 | 81 | final authState$ = authenticationState$.castAsNullable().publishState(null); 82 | 83 | final message$ = Rx.merge([logoutMessage$, updateAvatarMessage$]) 84 | .whereNotNull() 85 | .publish(); 86 | 87 | return HomeBloc._( 88 | changeAvatar: () => changeAvatarS.add(null), 89 | logout: () => logoutS.add(null), 90 | authState$: authState$, 91 | isUploading$: isUploading$, 92 | dispose: DisposeBag([ 93 | authState$.connect(), 94 | message$.connect(), 95 | changeAvatarS, 96 | logoutS, 97 | isUploading$, 98 | ]).dispose, 99 | message$: message$, 100 | ); 101 | } 102 | 103 | static LogoutMessage? _resultToLogoutMessage(UnitResult result) { 104 | return result.fold( 105 | ifRight: (_) => const LogoutSuccessMessage(), 106 | ifLeft: (appError) => appError.isCancellation 107 | ? null 108 | : LogoutErrorMessage(appError.message!, appError.error!), 109 | ); 110 | } 111 | 112 | static UpdateAvatarMessage? _resultToChangeAvatarMessage(UnitResult result) { 113 | return result.fold( 114 | ifRight: (_) => const UpdateAvatarSuccessMessage(), 115 | ifLeft: (appError) => appError.isCancellation 116 | ? null 117 | : UpdateAvatarErrorMessage(appError.message!, appError.error!), 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/pages/home/home_profile_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart'; 3 | import 'package:node_auth/data/constants.dart'; 4 | import 'package:node_auth/domain/models/app_error.dart'; 5 | import 'package:node_auth/domain/models/auth_state.dart'; 6 | import 'package:node_auth/domain/models/user.dart'; 7 | import 'package:node_auth/pages/home/home_bloc.dart'; 8 | import 'package:octo_image/octo_image.dart'; 9 | 10 | class HomeUserProfile extends StatelessWidget { 11 | const HomeUserProfile({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final homeBloc = BlocProvider.of(context); 16 | 17 | return Card( 18 | color: Colors.black.withOpacity(0.5), 19 | child: Padding( 20 | padding: const EdgeInsets.all(16.0), 21 | child: RxStreamBuilder?>( 22 | stream: homeBloc.authState$, 23 | builder: (context, result) { 24 | if (result == null) { 25 | return const Center( 26 | child: CircularProgressIndicator(), 27 | ); 28 | } 29 | final user = result.orNull()?.userAndToken?.user; 30 | return user == null 31 | ? _buildUnauthenticated(context) 32 | : RxStreamBuilder( 33 | stream: homeBloc.isUploading$, 34 | builder: (context, isUploading) => 35 | _buildProfile(user, homeBloc, context, isUploading), 36 | ); 37 | }, 38 | ), 39 | ), 40 | ); 41 | } 42 | 43 | Widget _buildProfile( 44 | User user, 45 | HomeBloc homeBloc, 46 | BuildContext context, 47 | bool isUploading, 48 | ) { 49 | final imageUrl = user.imageUrl; 50 | 51 | final provider = imageUrl != null 52 | ? NetworkImage( 53 | Uri.https( 54 | baseUrl, 55 | imageUrl, 56 | ).toString(), 57 | ) as ImageProvider 58 | : const AssetImage('assets/user.png'); 59 | 60 | final image = OctoImage( 61 | image: provider, 62 | fit: BoxFit.cover, 63 | width: 90.0, 64 | height: 90.0, 65 | progressIndicatorBuilder: (_, __) => const Center( 66 | child: CircularProgressIndicator( 67 | strokeWidth: 2, 68 | ), 69 | ), 70 | errorBuilder: (context, e, st) { 71 | debugPrint('$e $st'); 72 | final themeData = Theme.of(context); 73 | 74 | return Center( 75 | child: Column( 76 | mainAxisAlignment: MainAxisAlignment.center, 77 | children: [ 78 | Icon( 79 | Icons.error, 80 | color: themeData.colorScheme.secondary, 81 | ), 82 | const SizedBox(height: 4), 83 | Text( 84 | 'Error', 85 | style: themeData.textTheme.titleSmall!.copyWith(fontSize: 12), 86 | ), 87 | ], 88 | ), 89 | ); 90 | }, 91 | ); 92 | 93 | return Row( 94 | crossAxisAlignment: CrossAxisAlignment.center, 95 | children: [ 96 | if (isUploading) 97 | const ImageUploadingWidget() 98 | else 99 | ClipOval( 100 | child: GestureDetector( 101 | onTap: homeBloc.changeAvatar, 102 | child: image, 103 | ), 104 | ), 105 | Expanded( 106 | child: ListTile( 107 | title: Text( 108 | user.name, 109 | style: const TextStyle( 110 | fontSize: 24.0, 111 | fontWeight: FontWeight.w600, 112 | ), 113 | ), 114 | subtitle: Text( 115 | '${user.email}\n${user.createdAt}', 116 | style: const TextStyle( 117 | fontSize: 16.0, 118 | fontWeight: FontWeight.w400, 119 | fontStyle: FontStyle.italic, 120 | ), 121 | ), 122 | ), 123 | ), 124 | ], 125 | ); 126 | } 127 | 128 | Widget _buildUnauthenticated(BuildContext context) { 129 | return Center( 130 | child: Row( 131 | mainAxisAlignment: MainAxisAlignment.center, 132 | children: [ 133 | const Padding( 134 | padding: EdgeInsets.all(8.0), 135 | child: CircularProgressIndicator( 136 | valueColor: AlwaysStoppedAnimation(Colors.white), 137 | strokeWidth: 2, 138 | ), 139 | ), 140 | Expanded( 141 | child: Text( 142 | 'Logging out...', 143 | style: Theme.of(context).textTheme.titleMedium, 144 | textAlign: TextAlign.center, 145 | ), 146 | ), 147 | ], 148 | ), 149 | ); 150 | } 151 | } 152 | 153 | class ImageUploadingWidget extends StatelessWidget { 154 | const ImageUploadingWidget({super.key}); 155 | 156 | @override 157 | Widget build(BuildContext context) { 158 | return SizedBox( 159 | width: 90, 160 | height: 90, 161 | child: Column( 162 | crossAxisAlignment: CrossAxisAlignment.stretch, 163 | mainAxisAlignment: MainAxisAlignment.center, 164 | children: [ 165 | const Center( 166 | child: CircularProgressIndicator( 167 | strokeWidth: 2, 168 | ), 169 | ), 170 | const SizedBox(height: 8), 171 | Text( 172 | 'Uploading', 173 | style: Theme.of(context).textTheme.labelSmall, 174 | textAlign: TextAlign.center, 175 | ), 176 | ], 177 | ), 178 | ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /lib/pages/home/home_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | @immutable 4 | sealed class HomeMessage {} 5 | 6 | sealed class LogoutMessage implements HomeMessage {} 7 | 8 | sealed class UpdateAvatarMessage implements HomeMessage {} 9 | 10 | /// 11 | /// 12 | /// 13 | class LogoutSuccessMessage implements LogoutMessage { 14 | const LogoutSuccessMessage(); 15 | } 16 | 17 | class LogoutErrorMessage implements LogoutMessage { 18 | final String message; 19 | final Object error; 20 | 21 | const LogoutErrorMessage(this.message, this.error); 22 | 23 | @override 24 | String toString() => 'LogoutErrorMessage{message: $message, error: $error}'; 25 | } 26 | 27 | /// 28 | /// 29 | /// 30 | class UpdateAvatarSuccessMessage implements UpdateAvatarMessage { 31 | const UpdateAvatarSuccessMessage(); 32 | } 33 | 34 | class UpdateAvatarErrorMessage implements UpdateAvatarMessage { 35 | final String message; 36 | final Object error; 37 | 38 | const UpdateAvatarErrorMessage(this.message, this.error); 39 | 40 | @override 41 | String toString() => 42 | 'UpdateAvatarErrorMessage{message: $message, error: $error}'; 43 | } 44 | -------------------------------------------------------------------------------- /lib/pages/login/login.dart: -------------------------------------------------------------------------------- 1 | export 'login_page.dart'; 2 | export 'login_bloc.dart'; 3 | export 'login_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/pages/login/login_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:disposebag/disposebag.dart'; 4 | import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart'; 5 | import 'package:node_auth/domain/models/app_error.dart'; 6 | import 'package:node_auth/domain/usecases/login_use_case.dart'; 7 | import 'package:node_auth/pages/login/login.dart'; 8 | import 'package:node_auth/utils/streams.dart'; 9 | import 'package:node_auth/utils/type_defs.dart'; 10 | import 'package:node_auth/utils/validators.dart'; 11 | 12 | // ignore_for_file: close_sinks 13 | 14 | /// BLoC that handles validating form and login 15 | class LoginBloc extends DisposeCallbackBaseBloc { 16 | /// Input functions 17 | final Function1 emailChanged; 18 | final Function1 passwordChanged; 19 | final Function0 submitLogin; 20 | 21 | /// Streams 22 | final Stream emailError$; 23 | final Stream passwordError$; 24 | final Stream message$; 25 | final Stream isLoading$; 26 | 27 | LoginBloc._({ 28 | required Function0 dispose, 29 | required this.emailChanged, 30 | required this.passwordChanged, 31 | required this.submitLogin, 32 | required this.emailError$, 33 | required this.passwordError$, 34 | required this.message$, 35 | required this.isLoading$, 36 | }) : super(dispose); 37 | 38 | factory LoginBloc(final LoginUseCase login) { 39 | /// Controllers 40 | final emailController = PublishSubject(); 41 | final passwordController = PublishSubject(); 42 | final submitLoginController = PublishSubject(); 43 | final isLoadingController = BehaviorSubject.seeded(false); 44 | final controllers = [ 45 | emailController, 46 | passwordController, 47 | submitLoginController, 48 | isLoadingController, 49 | ]; 50 | 51 | /// 52 | /// Streams 53 | /// 54 | 55 | final isValidSubmit$ = Rx.combineLatest3( 56 | emailController.stream.map(Validator.isValidEmail), 57 | passwordController.stream.map(Validator.isValidPassword), 58 | isLoadingController.stream, 59 | (bool isValidEmail, bool isValidPassword, bool isLoading) => 60 | isValidEmail && isValidPassword && !isLoading, 61 | ).shareValueSeeded(false); 62 | 63 | final credential$ = Rx.combineLatest2( 64 | emailController.stream, 65 | passwordController.stream, 66 | (String email, String password) => 67 | Credential(email: email, password: password), 68 | ); 69 | 70 | final submit$ = submitLoginController.stream 71 | .withLatestFrom(isValidSubmit$, (_, bool isValid) => isValid) 72 | .share(); 73 | 74 | final message$ = Rx.merge([ 75 | submit$ 76 | .where((isValid) => isValid) 77 | .withLatestFrom(credential$, (_, Credential c) => c) 78 | .exhaustMap( 79 | (credential) => login( 80 | email: credential.email, 81 | password: credential.password, 82 | ) 83 | .doOn( 84 | listen: () => isLoadingController.add(true), 85 | cancel: () => isLoadingController.add(false), 86 | ) 87 | .map(_responseToMessage), 88 | ), 89 | submit$ 90 | .where((isValid) => !isValid) 91 | .map((_) => const InvalidInformationMessage()) 92 | ]).whereNotNull().share(); 93 | 94 | final emailError$ = emailController.stream 95 | .map((email) { 96 | if (Validator.isValidEmail(email)) return null; 97 | return 'Invalid email address'; 98 | }) 99 | .distinct() 100 | .share(); 101 | 102 | final passwordError$ = passwordController.stream 103 | .map((password) { 104 | if (Validator.isValidPassword(password)) return null; 105 | return 'Password must be at least 6 characters'; 106 | }) 107 | .distinct() 108 | .share(); 109 | 110 | final subscriptions = >{ 111 | 'emailError': emailError$, 112 | 'passwordError': passwordError$, 113 | 'isValidSubmit': isValidSubmit$, 114 | 'message': message$, 115 | 'isLoading': isLoadingController, 116 | }.debug(); 117 | 118 | return LoginBloc._( 119 | dispose: DisposeBag([...controllers, ...subscriptions]).dispose, 120 | emailChanged: trim.pipe(emailController.add), 121 | passwordChanged: passwordController.add, 122 | submitLogin: () => submitLoginController.add(null), 123 | emailError$: emailError$, 124 | passwordError$: passwordError$, 125 | message$: message$, 126 | isLoading$: isLoadingController, 127 | ); 128 | } 129 | 130 | static LoginMessage? _responseToMessage(UnitResult result) { 131 | return result.fold( 132 | ifRight: (_) => const LoginSuccessMessage(), 133 | ifLeft: (appError) => appError.isCancellation 134 | ? null 135 | : LoginErrorMessage(appError.message!, appError.error!), 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/pages/login/login_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// 4 | /// Login message 5 | /// 6 | 7 | class Credential { 8 | final String email; 9 | final String password; 10 | 11 | const Credential({required this.email, required this.password}); 12 | } 13 | 14 | @immutable 15 | sealed class LoginMessage {} 16 | 17 | class LoginSuccessMessage implements LoginMessage { 18 | const LoginSuccessMessage(); 19 | } 20 | 21 | class LoginErrorMessage implements LoginMessage { 22 | final Object error; 23 | final String message; 24 | 25 | const LoginErrorMessage(this.message, this.error); 26 | 27 | @override 28 | String toString() => 'LoginErrorMessage{message=$message, error=$error}'; 29 | } 30 | 31 | class InvalidInformationMessage implements LoginMessage { 32 | const InvalidInformationMessage(); 33 | } 34 | -------------------------------------------------------------------------------- /lib/pages/register/register.dart: -------------------------------------------------------------------------------- 1 | export 'register_page.dart'; 2 | export 'register_state.dart'; 3 | export 'register_bloc.dart'; 4 | -------------------------------------------------------------------------------- /lib/pages/register/register_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:disposebag/disposebag.dart'; 4 | import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart'; 5 | import 'package:node_auth/domain/models/app_error.dart'; 6 | import 'package:node_auth/domain/usecases/register_use_case.dart'; 7 | import 'package:node_auth/pages/register/register.dart'; 8 | import 'package:node_auth/utils/streams.dart'; 9 | import 'package:node_auth/utils/type_defs.dart'; 10 | import 'package:node_auth/utils/validators.dart'; 11 | 12 | // ignore_for_file: close_sinks 13 | 14 | /// BLoC handles validating form and register 15 | class RegisterBloc extends DisposeCallbackBaseBloc { 16 | /// Input functions 17 | final Function1 nameChanged; 18 | final Function1 emailChanged; 19 | final Function1 passwordChanged; 20 | final Function0 submitRegister; 21 | 22 | /// Streams 23 | final Stream emailError$; 24 | final Stream passwordError$; 25 | final Stream nameError$; 26 | final Stream message$; 27 | final Stream isLoading$; 28 | 29 | RegisterBloc._({ 30 | required Function0 dispose, 31 | required this.emailChanged, 32 | required this.passwordChanged, 33 | required this.submitRegister, 34 | required this.emailError$, 35 | required this.passwordError$, 36 | required this.message$, 37 | required this.isLoading$, 38 | required this.nameChanged, 39 | required this.nameError$, 40 | }) : super(dispose); 41 | 42 | factory RegisterBloc(final RegisterUseCase registerUser) { 43 | /// Controllers 44 | final emailController = PublishSubject(); 45 | final nameController = PublishSubject(); 46 | final passwordController = PublishSubject(); 47 | final submitRegisterController = PublishSubject(); 48 | final isLoadingController = BehaviorSubject.seeded(false); 49 | final controllers = [ 50 | emailController, 51 | nameController, 52 | passwordController, 53 | submitRegisterController, 54 | isLoadingController, 55 | ]; 56 | 57 | /// 58 | /// Streams 59 | /// 60 | 61 | final isValidSubmit$ = Rx.combineLatest4( 62 | emailController.stream.map(Validator.isValidEmail), 63 | passwordController.stream.map(Validator.isValidPassword), 64 | isLoadingController.stream, 65 | nameController.stream.map(Validator.isValidUserName), 66 | (bool isValidEmail, bool isValidPassword, bool isLoading, 67 | bool isValidName) => 68 | isValidEmail && isValidPassword && !isLoading && isValidName, 69 | ).shareValueSeeded(false); 70 | 71 | final registerUser$ = Rx.combineLatest3( 72 | emailController.stream, 73 | passwordController.stream, 74 | nameController.stream, 75 | (String email, String password, String name) => 76 | RegisterUser(email, name, password), 77 | ); 78 | 79 | final submit$ = submitRegisterController.stream 80 | .withLatestFrom(isValidSubmit$, (_, bool isValid) => isValid) 81 | .share(); 82 | 83 | final message$ = Rx.merge([ 84 | submit$ 85 | .where((isValid) => isValid) 86 | .withLatestFrom(registerUser$, (_, RegisterUser user) => user) 87 | .exhaustMap( 88 | (user) => registerUser( 89 | email: user.email, 90 | password: user.password, 91 | name: user.name, 92 | ) 93 | .doOn( 94 | listen: () => isLoadingController.add(true), 95 | cancel: () => isLoadingController.add(false), 96 | ) 97 | .map((result) => _responseToMessage(result, user.email)), 98 | ), 99 | submit$ 100 | .where((isValid) => !isValid) 101 | .map((_) => const RegisterInvalidInformationMessage()) 102 | ]).whereNotNull().share(); 103 | 104 | final emailError$ = emailController.stream 105 | .map((email) { 106 | if (Validator.isValidEmail(email)) return null; 107 | return 'Invalid email address'; 108 | }) 109 | .distinct() 110 | .share(); 111 | 112 | final passwordError$ = passwordController.stream 113 | .map((password) { 114 | if (Validator.isValidPassword(password)) return null; 115 | return 'Password must be at least 6 characters'; 116 | }) 117 | .distinct() 118 | .share(); 119 | 120 | final nameError$ = nameController.stream 121 | .map((name) { 122 | if (Validator.isValidUserName(name)) return null; 123 | return 'Name must be at least 3 characters'; 124 | }) 125 | .distinct() 126 | .share(); 127 | 128 | final subscriptions = >{ 129 | 'emailError': emailError$, 130 | 'passwordError': passwordError$, 131 | 'nameError': nameError$, 132 | 'isValidSubmit': isValidSubmit$, 133 | 'message': message$, 134 | 'isLoading': isLoadingController, 135 | }.debug(); 136 | 137 | return RegisterBloc._( 138 | dispose: DisposeBag([...subscriptions, ...controllers]).dispose, 139 | nameChanged: trim.pipe(nameController.add), 140 | emailChanged: trim.pipe(emailController.add), 141 | passwordChanged: passwordController.add, 142 | submitRegister: () => submitRegisterController.add(null), 143 | emailError$: emailError$, 144 | passwordError$: passwordError$, 145 | message$: message$, 146 | isLoading$: isLoadingController, 147 | nameError$: nameError$, 148 | ); 149 | } 150 | 151 | static RegisterMessage? _responseToMessage(UnitResult result, String email) { 152 | return result.fold( 153 | ifRight: (_) => RegisterSuccessMessage(email), 154 | ifLeft: (appError) => appError.isCancellation 155 | ? null 156 | : RegisterErrorMessage(appError.message!, appError.error!), 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /lib/pages/register/register_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | @immutable 4 | class RegisterUser { 5 | final String email; 6 | final String name; 7 | final String password; 8 | 9 | const RegisterUser(this.email, this.name, this.password); 10 | } 11 | 12 | @immutable 13 | sealed class RegisterMessage {} 14 | 15 | class RegisterInvalidInformationMessage implements RegisterMessage { 16 | const RegisterInvalidInformationMessage(); 17 | } 18 | 19 | class RegisterErrorMessage implements RegisterMessage { 20 | final String message; 21 | final Object error; 22 | 23 | const RegisterErrorMessage(this.message, this.error); 24 | } 25 | 26 | class RegisterSuccessMessage implements RegisterMessage { 27 | final String email; 28 | 29 | const RegisterSuccessMessage(this.email); 30 | } 31 | -------------------------------------------------------------------------------- /lib/pages/reset_password/input_token/input_token_and_reset_password.dart: -------------------------------------------------------------------------------- 1 | export 'input_token_and_reset_password_bloc.dart'; 2 | export 'input_token_and_reset_password_page.dart'; 3 | -------------------------------------------------------------------------------- /lib/pages/reset_password/input_token/input_token_and_reset_password_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:disposebag/disposebag.dart'; 4 | import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart'; 5 | import 'package:meta/meta.dart'; 6 | import 'package:node_auth/domain/usecases/reset_password_use_case.dart'; 7 | import 'package:node_auth/utils/type_defs.dart'; 8 | import 'package:node_auth/utils/validators.dart'; 9 | import 'package:rxdart_ext/rxdart_ext.dart'; 10 | 11 | @immutable 12 | sealed class InputTokenAndResetPasswordMessage {} 13 | 14 | class InvalidInformation implements InputTokenAndResetPasswordMessage { 15 | const InvalidInformation(); 16 | } 17 | 18 | class ResetPasswordSuccess implements InputTokenAndResetPasswordMessage { 19 | final String email; 20 | 21 | const ResetPasswordSuccess(this.email); 22 | } 23 | 24 | class ResetPasswordFailure implements InputTokenAndResetPasswordMessage { 25 | final String message; 26 | final Object error; 27 | 28 | const ResetPasswordFailure(this.error, this.message); 29 | } 30 | 31 | typedef _FormInfo = ({String email, String token, String newPassword}); 32 | 33 | //ignore_for_file: close_sinks 34 | 35 | class InputTokenAndResetPasswordBloc extends DisposeCallbackBaseBloc { 36 | final Function1 emailChanged; 37 | final Function1 passwordChanged; 38 | final Function1 tokenChanged; 39 | final Function0 submit; 40 | 41 | final Stream emailError$; 42 | final Stream passwordError$; 43 | final Stream tokenError$; 44 | final Stream isLoading$; 45 | final Stream message$; 46 | 47 | InputTokenAndResetPasswordBloc._({ 48 | required this.emailChanged, 49 | required this.passwordChanged, 50 | required this.tokenChanged, 51 | required this.emailError$, 52 | required this.passwordError$, 53 | required this.tokenError$, 54 | required Function0 dispose, 55 | required this.submit, 56 | required this.isLoading$, 57 | required this.message$, 58 | }) : super(dispose); 59 | 60 | factory InputTokenAndResetPasswordBloc( 61 | final ResetPasswordUseCase resetPassword) { 62 | final emailSubject = BehaviorSubject.seeded(''); 63 | final tokenSubject = BehaviorSubject.seeded(''); 64 | final passwordSubject = BehaviorSubject.seeded(''); 65 | final submitSubject = PublishSubject(); 66 | final isLoadingSubject = BehaviorSubject.seeded(false); 67 | final subjects = [ 68 | emailSubject, 69 | tokenSubject, 70 | passwordSubject, 71 | submitSubject, 72 | isLoadingSubject, 73 | ]; 74 | 75 | /// 76 | /// Stream 77 | /// 78 | final emailError$ = emailSubject.map((email) { 79 | if (Validator.isValidEmail(email)) return null; 80 | return 'Invalid email address'; 81 | }).share(); 82 | 83 | final passwordError$ = passwordSubject.map((password) { 84 | if (Validator.isValidPassword(password)) return null; 85 | return 'Password must be at least 6 characters'; 86 | }).share(); 87 | 88 | final tokenError$ = tokenSubject.map((token) { 89 | if (token.isNotEmpty) return null; 90 | return 'Token must be not empty'; 91 | }).share(); 92 | 93 | final Stream<_FormInfo> allField$ = submitSubject 94 | .map( 95 | (_) => ( 96 | email: emailSubject.value, 97 | token: tokenSubject.value, 98 | newPassword: passwordSubject.value 99 | ), 100 | ) 101 | .share(); 102 | 103 | bool allFieldsAreValid(_FormInfo formInfo) => 104 | Validator.isValidEmail(formInfo.email) && 105 | formInfo.token.isNotEmpty && 106 | Validator.isValidPassword(formInfo.newPassword); 107 | 108 | final message$ = Rx.merge([ 109 | allField$ 110 | .where((formInfo) => !allFieldsAreValid(formInfo)) 111 | .map((_) => const InvalidInformation()), 112 | allField$ 113 | .where(allFieldsAreValid) 114 | .exhaustMap((formInfo) => _sendResetPasswordRequest( 115 | resetPassword, 116 | formInfo, 117 | isLoadingSubject, 118 | )), 119 | ]).whereNotNull().share(); 120 | 121 | return InputTokenAndResetPasswordBloc._( 122 | dispose: DisposeBag(subjects).dispose, 123 | emailChanged: trim.pipe(emailSubject.add), 124 | tokenChanged: tokenSubject.add, 125 | passwordChanged: passwordSubject.add, 126 | submit: () => submitSubject.add(null), 127 | passwordError$: passwordError$, 128 | emailError$: emailError$, 129 | isLoading$: isLoadingSubject, 130 | tokenError$: tokenError$, 131 | message$: message$, 132 | ); 133 | } 134 | 135 | static Stream _sendResetPasswordRequest( 136 | ResetPasswordUseCase resetPassword, 137 | _FormInfo formInfo, 138 | Sink isLoadingSink, 139 | ) { 140 | return resetPassword( 141 | email: formInfo.email, 142 | token: formInfo.token, 143 | newPassword: formInfo.newPassword, 144 | ) 145 | .doOn( 146 | listen: () => isLoadingSink.add(true), 147 | cancel: () => isLoadingSink.add(false), 148 | ) 149 | .map( 150 | (result) => result.fold( 151 | ifRight: (_) => ResetPasswordSuccess(formInfo.email), 152 | ifLeft: (appError) => appError.isCancellation 153 | ? null 154 | : ResetPasswordFailure(appError.error!, appError.message!), 155 | ), 156 | ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib/pages/reset_password/reset_password_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart'; 5 | import 'package:flutter_disposebag/flutter_disposebag.dart'; 6 | import 'package:flutter_provider/flutter_provider.dart'; 7 | import 'package:node_auth/domain/usecases/reset_password_use_case.dart'; 8 | import 'package:node_auth/domain/usecases/send_reset_password_email_use_case.dart'; 9 | import 'package:node_auth/pages/reset_password/input_token/input_token_and_reset_password.dart'; 10 | import 'package:node_auth/pages/reset_password/send_email/send_email.dart'; 11 | import 'package:rxdart_ext/rxdart_ext.dart'; 12 | 13 | class ResetPasswordPage extends StatefulWidget { 14 | static const routeName = '/reset_password_page'; 15 | 16 | const ResetPasswordPage({super.key}); 17 | 18 | @override 19 | State createState() => _ResetPasswordPageState(); 20 | } 21 | 22 | class _ResetPasswordPageState extends State 23 | with SingleTickerProviderStateMixin, DisposeBagMixin { 24 | final requestEmailS = StreamController(sync: true); 25 | late final StateStream requestEmail$; 26 | 27 | late final AnimationController animationController; 28 | late final Animation animationPosition; 29 | late final Animation animationScale; 30 | late final Animation animationOpacity; 31 | late final Animation animationTurns; 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | 37 | animationController = AnimationController( 38 | vsync: this, 39 | duration: const Duration(milliseconds: 1000), 40 | ); 41 | animationPosition = Tween( 42 | begin: const Offset(2.0, 0), 43 | end: const Offset(0, 0), 44 | ).animate( 45 | CurvedAnimation( 46 | parent: animationController, 47 | curve: Curves.easeOut, 48 | ), 49 | ); 50 | animationScale = Tween( 51 | begin: 0.0, 52 | end: 1.0, 53 | ).animate( 54 | CurvedAnimation( 55 | parent: animationController, 56 | curve: Curves.fastOutSlowIn, 57 | ), 58 | ); 59 | animationOpacity = Tween( 60 | begin: 0.0, 61 | end: 1.0, 62 | ).animate( 63 | CurvedAnimation( 64 | parent: animationController, 65 | curve: Curves.fastOutSlowIn, 66 | ), 67 | ); 68 | animationTurns = Tween( 69 | begin: 0.5, 70 | end: 0, 71 | ).animate( 72 | CurvedAnimation( 73 | parent: animationController, 74 | curve: Curves.easeOut, 75 | ), 76 | ); 77 | 78 | /// Stream of bool values. 79 | /// Emits true if current page is request email page. 80 | /// Otherwise, it is reset password page. 81 | requestEmail$ = requestEmailS.stream 82 | .scan((acc, e, _) => !acc, true) 83 | .doOnData((requestEmailPage) => requestEmailPage 84 | ? animationController.reverse() 85 | : animationController.forward()) 86 | .publishState(true) 87 | ..connect().disposedBy(bag); 88 | requestEmailS.disposedBy(bag); 89 | } 90 | 91 | @override 92 | void dispose() { 93 | animationController.dispose(); 94 | super.dispose(); 95 | } 96 | 97 | void onToggle() => requestEmailS.add(null); 98 | 99 | @override 100 | Widget build(BuildContext context) { 101 | final sendEmailPage = BlocProvider( 102 | initBloc: (context) => SendEmailBloc( 103 | SendResetPasswordEmailUseCase(context.get()), 104 | ), 105 | child: SendEmailPage(toggle: onToggle), 106 | ); 107 | 108 | final resetPasswordPage = BlocProvider( 109 | initBloc: (context) => InputTokenAndResetPasswordBloc( 110 | ResetPasswordUseCase(context.get()), 111 | ), 112 | child: InputTokenAndResetPasswordPage(toggle: onToggle), 113 | ); 114 | 115 | return Scaffold( 116 | appBar: AppBar( 117 | title: RxStreamBuilder( 118 | stream: requestEmail$, 119 | builder: (context, requestEmailPage) { 120 | return Text(requestEmailPage ? 'Request email' : 'Reset password'); 121 | }, 122 | ), 123 | ), 124 | body: Stack( 125 | children: [ 126 | Positioned.fill(child: sendEmailPage), 127 | Positioned.fill( 128 | child: RotationTransition( 129 | turns: animationTurns, 130 | child: SlideTransition( 131 | position: animationPosition, 132 | child: ScaleTransition( 133 | scale: animationScale, 134 | child: FadeTransition( 135 | opacity: animationOpacity, 136 | child: resetPasswordPage, 137 | ), 138 | ), 139 | ), 140 | ), 141 | ) 142 | ], 143 | ), 144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/pages/reset_password/send_email/send_email.dart: -------------------------------------------------------------------------------- 1 | export 'send_email_bloc.dart'; 2 | export 'send_email_page.dart'; 3 | export 'send_email_state.dart'; 4 | -------------------------------------------------------------------------------- /lib/pages/reset_password/send_email/send_email_bloc.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: close_sinks 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:disposebag/disposebag.dart'; 6 | import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart'; 7 | import 'package:node_auth/domain/usecases/send_reset_password_email_use_case.dart'; 8 | import 'package:node_auth/pages/reset_password/send_email/send_email.dart'; 9 | import 'package:node_auth/utils/type_defs.dart'; 10 | import 'package:node_auth/utils/validators.dart'; 11 | import 'package:rxdart_ext/rxdart_ext.dart'; 12 | 13 | class SendEmailBloc extends DisposeCallbackBaseBloc { 14 | /// 15 | final Function0 submit; 16 | final Function1 emailChanged; 17 | 18 | /// 19 | final Stream emailError$; 20 | final Stream message$; 21 | final Stream isLoading$; 22 | 23 | SendEmailBloc._({ 24 | required this.submit, 25 | required this.emailChanged, 26 | required this.emailError$, 27 | required this.message$, 28 | required this.isLoading$, 29 | required Function0 dispose, 30 | }) : super(dispose); 31 | 32 | factory SendEmailBloc( 33 | final SendResetPasswordEmailUseCase sendResetPasswordEmail) { 34 | final emailS = PublishSubject(); 35 | final submitS = PublishSubject(); 36 | final isLoadingS = BehaviorSubject.seeded(false); 37 | 38 | final emailError$ = emailS.map((email) { 39 | if (Validator.isValidEmail(email)) return null; 40 | return 'Invalid email address'; 41 | }).share(); 42 | 43 | final submittedEmail$ = 44 | submitS.withLatestFrom(emailS, (_, String email) => email).share(); 45 | 46 | final message$ = Rx.merge([ 47 | submittedEmail$ 48 | .where((email) => !Validator.isValidEmail(email)) 49 | .map((_) => const SendEmailInvalidInformationMessage()), 50 | submittedEmail$.where(Validator.isValidEmail).exhaustMap( 51 | (email) { 52 | return send( 53 | email, 54 | sendResetPasswordEmail, 55 | isLoadingS, 56 | ); 57 | }, 58 | ), 59 | ]).whereNotNull().share(); 60 | 61 | return SendEmailBloc._( 62 | dispose: DisposeBag([emailS, submitS, isLoadingS]).dispose, 63 | emailChanged: trim.pipe(emailS.add), 64 | emailError$: emailError$, 65 | submit: () => submitS.add(null), 66 | message$: message$, 67 | isLoading$: isLoadingS, 68 | ); 69 | } 70 | 71 | static Stream send( 72 | String email, 73 | SendResetPasswordEmailUseCase sendResetPasswordEmail, 74 | Sink isLoadingController, 75 | ) { 76 | return sendResetPasswordEmail(email) 77 | .doOn( 78 | listen: () => isLoadingController.add(true), 79 | cancel: () => isLoadingController.add(false), 80 | ) 81 | .map( 82 | (result) => result.fold( 83 | ifRight: (_) => const SendEmailSuccessMessage(), 84 | ifLeft: (appError) => appError.isCancellation 85 | ? null 86 | : SendEmailErrorMessage(appError.error!, appError.message!), 87 | ), 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/pages/reset_password/send_email/send_email_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart'; 3 | import 'package:flutter_disposebag/flutter_disposebag.dart'; 4 | import 'package:node_auth/pages/reset_password/send_email/send_email.dart'; 5 | import 'package:node_auth/utils/snackbar.dart'; 6 | 7 | class SendEmailPage extends StatefulWidget { 8 | final VoidCallback toggle; 9 | 10 | const SendEmailPage({super.key, required this.toggle}); 11 | 12 | @override 13 | State createState() => _SendEmailPageState(); 14 | } 15 | 16 | class _SendEmailPageState extends State 17 | with SingleTickerProviderStateMixin, DisposeBagMixin { 18 | late final AnimationController fadeController; 19 | late final Animation fadeAnim; 20 | Object? listen; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | 26 | fadeController = AnimationController( 27 | vsync: this, 28 | duration: const Duration(milliseconds: 800), 29 | ); 30 | fadeAnim = Tween(begin: 0, end: 1).animate( 31 | CurvedAnimation( 32 | curve: Curves.fastOutSlowIn, 33 | parent: fadeController, 34 | ), 35 | ); 36 | } 37 | 38 | @override 39 | void didChangeDependencies() { 40 | super.didChangeDependencies(); 41 | 42 | listen ??= [ 43 | context 44 | .bloc() 45 | .message$ 46 | .map(_getMessageString) 47 | .listen(context.showSnackBar), 48 | context.bloc().isLoading$.listen((isLoading) { 49 | if (isLoading) { 50 | fadeController.forward(); 51 | } else { 52 | fadeController.reverse(); 53 | } 54 | }), 55 | ].disposedBy(bag); 56 | } 57 | 58 | @override 59 | void dispose() { 60 | fadeController.dispose(); 61 | super.dispose(); 62 | } 63 | 64 | @override 65 | Widget build(BuildContext context) { 66 | final bloc = BlocProvider.of(context); 67 | 68 | final emailTextField = StreamBuilder( 69 | stream: bloc.emailError$, 70 | builder: (context, snapshot) { 71 | return TextField( 72 | autocorrect: true, 73 | decoration: InputDecoration( 74 | prefixIcon: const Padding( 75 | padding: EdgeInsetsDirectional.only(end: 8.0), 76 | child: Icon(Icons.email), 77 | ), 78 | labelText: 'Email', 79 | errorText: snapshot.data, 80 | ), 81 | keyboardType: TextInputType.emailAddress, 82 | maxLines: 1, 83 | autofocus: true, 84 | onChanged: bloc.emailChanged, 85 | textInputAction: TextInputAction.done, 86 | onSubmitted: (_) { 87 | FocusScope.of(context).unfocus(); 88 | }, 89 | style: const TextStyle(fontSize: 16.0), 90 | ); 91 | }, 92 | ); 93 | 94 | final overlayColor = WidgetStateProperty.resolveWith((states) { 95 | if (states.contains(WidgetState.hovered)) { 96 | return Theme.of(context).colorScheme.secondary.withOpacity(0.5); 97 | } 98 | if (states.contains(WidgetState.focused) || 99 | states.contains(WidgetState.pressed)) { 100 | return Theme.of(context).colorScheme.secondary.withOpacity(0.8); 101 | } 102 | return null; 103 | }); 104 | 105 | final buttonStyle = ElevatedButton.styleFrom( 106 | padding: const EdgeInsets.all(16), 107 | shape: RoundedRectangleBorder( 108 | borderRadius: BorderRadius.circular(8), 109 | ), 110 | backgroundColor: Theme.of(context).cardColor, 111 | ).copyWith(overlayColor: overlayColor); 112 | 113 | return Container( 114 | decoration: BoxDecoration( 115 | image: DecorationImage( 116 | image: const AssetImage('assets/bg.jpg'), 117 | fit: BoxFit.cover, 118 | colorFilter: ColorFilter.mode( 119 | Colors.black.withAlpha(0xBF), 120 | BlendMode.darken, 121 | ), 122 | ), 123 | ), 124 | child: Center( 125 | child: SingleChildScrollView( 126 | child: Column( 127 | mainAxisSize: MainAxisSize.min, 128 | crossAxisAlignment: CrossAxisAlignment.stretch, 129 | mainAxisAlignment: MainAxisAlignment.center, 130 | children: [ 131 | Padding( 132 | padding: const EdgeInsets.all(8.0), 133 | child: emailTextField, 134 | ), 135 | Center( 136 | child: FadeTransition( 137 | opacity: fadeAnim, 138 | child: const Padding( 139 | padding: EdgeInsets.all(24), 140 | child: CircularProgressIndicator( 141 | strokeWidth: 2, 142 | ), 143 | ), 144 | ), 145 | ), 146 | Container( 147 | margin: const EdgeInsets.symmetric(horizontal: 16), 148 | child: ElevatedButton( 149 | style: buttonStyle, 150 | onPressed: bloc.submit, 151 | child: const Text('Send'), 152 | ), 153 | ), 154 | const SizedBox(height: 8), 155 | Container( 156 | margin: const EdgeInsets.symmetric(horizontal: 16), 157 | child: ElevatedButton( 158 | style: buttonStyle, 159 | onPressed: widget.toggle, 160 | child: const Text('Input received token'), 161 | ), 162 | ), 163 | ], 164 | ), 165 | ), 166 | ), 167 | ); 168 | } 169 | 170 | static String _getMessageString(SendEmailMessage msg) { 171 | return switch (msg) { 172 | SendEmailInvalidInformationMessage() => 'Invalid information. Try again', 173 | SendEmailSuccessMessage() => 174 | 'Email sent. Check your email inbox and go to reset password page', 175 | SendEmailErrorMessage() => msg.message, 176 | }; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/pages/reset_password/send_email/send_email_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | @immutable 4 | sealed class SendEmailMessage {} 5 | 6 | class SendEmailInvalidInformationMessage implements SendEmailMessage { 7 | const SendEmailInvalidInformationMessage(); 8 | } 9 | 10 | class SendEmailSuccessMessage implements SendEmailMessage { 11 | const SendEmailSuccessMessage(); 12 | } 13 | 14 | class SendEmailErrorMessage implements SendEmailMessage { 15 | final String message; 16 | final Object error; 17 | 18 | const SendEmailErrorMessage(this.error, this.message); 19 | } 20 | -------------------------------------------------------------------------------- /lib/utils/snackbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension ShowSnackBarBuildContextExtension on BuildContext { 4 | void showSnackBar( 5 | String message, [ 6 | Duration duration = const Duration(seconds: 1), 7 | ]) { 8 | final messengerState = ScaffoldMessenger.of(this); 9 | messengerState.hideCurrentSnackBar(); 10 | messengerState.showSnackBar( 11 | SnackBar( 12 | content: Text(message), 13 | duration: duration, 14 | ), 15 | ); 16 | } 17 | 18 | void hideCurrentSnackBar() { 19 | ScaffoldMessenger.of(this).hideCurrentSnackBar(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/utils/streams.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:rxdart_ext/rxdart_ext.dart'; 5 | import 'package:node_auth/utils/unit.dart'; 6 | import 'package:dart_either/dart_either.dart'; 7 | 8 | extension AsUnitSingleExtension on Single> { 9 | Single> asUnit() => map((r) => r.map((_) => Unit.instance)); 10 | } 11 | 12 | extension DebugMapStreamsExtension on Map> { 13 | List> debug() => entries 14 | .map((entry) => entry.value 15 | .listen((data) => debugPrint('[DEBUG] [${entry.key}] = $data'))) 16 | .toList(); 17 | } 18 | 19 | extension CastAsNullableStreamExtension on Stream { 20 | Stream castAsNullable() => cast(); 21 | } 22 | 23 | extension CastAsNullableSingleExtension on Single { 24 | Single castAsNullable() => cast(); 25 | } 26 | -------------------------------------------------------------------------------- /lib/utils/type_defs.dart: -------------------------------------------------------------------------------- 1 | typedef Function0 = T Function(); 2 | 3 | typedef Function1 = R Function(P0); 4 | 5 | typedef Function2 = R Function(P0, P1); 6 | 7 | typedef Function3 = R Function(P0, P1, P2); 8 | 9 | extension PipeFunction1Extension on Function1 { 10 | Function1 pipe(Function1 other) => (t) => other(this(t)); 11 | } 12 | 13 | String trim(String s) => s.trim(); 14 | -------------------------------------------------------------------------------- /lib/utils/unit.dart: -------------------------------------------------------------------------------- 1 | class Unit { 2 | const Unit._(); 3 | 4 | static const instance = Unit._(); 5 | } 6 | -------------------------------------------------------------------------------- /lib/utils/validators.dart: -------------------------------------------------------------------------------- 1 | class Validator { 2 | Validator._(); 3 | 4 | static const _emailRegExpString = 5 | r'[a-zA-Z0-9\+\.\_\%\-\+]{1,256}\@[a-zA-Z0-9]' 6 | r'[a-zA-Z0-9\-]{0,64}(\.[a-zA-Z0-9][a-zA-Z0-9\-]{0,25})+'; 7 | static final _emailRegex = RegExp(_emailRegExpString, caseSensitive: false); 8 | 9 | static bool isValidPassword(String password) => password.length >= 6; 10 | 11 | static bool isValidEmail(String email) => _emailRegex.hasMatch(email); 12 | 13 | static bool isValidUserName(String userName) => userName.length >= 3; 14 | } 15 | -------------------------------------------------------------------------------- /lib/widgets/password_textfield.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class PasswordTextField extends StatefulWidget { 4 | final ValueChanged onChanged; 5 | final String? errorText; 6 | final String labelText; 7 | final TextInputAction textInputAction; 8 | final VoidCallback onSubmitted; 9 | final FocusNode? focusNode; 10 | 11 | const PasswordTextField({ 12 | super.key, 13 | required this.onChanged, 14 | required this.errorText, 15 | required this.labelText, 16 | required this.onSubmitted, 17 | required this.textInputAction, 18 | required this.focusNode, 19 | }); 20 | 21 | @override 22 | State createState() => _PasswordTextFieldState(); 23 | } 24 | 25 | class _PasswordTextFieldState extends State { 26 | bool _obscureText = true; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return TextField( 31 | autocorrect: true, 32 | obscureText: _obscureText, 33 | decoration: InputDecoration( 34 | errorText: widget.errorText, 35 | suffixIcon: IconButton( 36 | onPressed: () => setState(() => _obscureText = !_obscureText), 37 | icon: Icon( 38 | _obscureText ? Icons.visibility_off : Icons.visibility, 39 | ), 40 | iconSize: 18.0, 41 | ), 42 | labelText: widget.labelText, 43 | prefixIcon: const Padding( 44 | padding: EdgeInsetsDirectional.only(end: 8.0), 45 | child: Icon(Icons.lock), 46 | ), 47 | ), 48 | keyboardType: TextInputType.text, 49 | maxLines: 1, 50 | style: const TextStyle(fontSize: 16.0), 51 | onChanged: widget.onChanged, 52 | onSubmitted: (_) => widget.onSubmitted(), 53 | textInputAction: widget.textInputAction, 54 | focusNode: widget.focusNode, 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: node_auth 2 | description: Simple auth app flutter, server node.js, BLoC pattern, RxDart. 3 | 4 | publish_to: 'none' 5 | version: 3.3.0+8 6 | 7 | environment: 8 | sdk: ^3.4.3 9 | flutter: ">=3.22.2" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | cupertino_icons: ^1.0.8 16 | 17 | cupertino_http: ^2.0.0 18 | meta: ^1.12.0 19 | path: ^1.8.2 20 | http: ^1.2.2 21 | built_value: ^8.9.2 22 | rxdart: ^0.28.0 23 | rxdart_flutter: ^0.0.1 24 | image_picker: ^1.1.2 25 | shared_preferences: ^2.3.2 26 | octo_image: ^2.1.0 27 | 28 | flutter_provider: ^2.1.0 29 | rx_shared_preferences: ^4.0.0 30 | disposebag: ^1.5.1 31 | flutter_disposebag: ^1.1.0 32 | flutter_bloc_pattern: ^3.0.0 33 | rxdart_ext: ^0.3.0 34 | did_change_dependencies: ^1.0.0 35 | stream_loader: ^1.5.0 36 | dart_either: ^2.0.0 37 | http_client_hoc081098: ^0.1.0 38 | cancellation_token_hoc081098: ^2.0.0 39 | 40 | dev_dependencies: 41 | plugin_platform_interface: ^2.1.3 42 | flutter_test: 43 | sdk: flutter 44 | build_runner: ^2.4.11 45 | built_value_generator: ^8.9.2 46 | analyzer: ^5.2.0 47 | flutter_lints: ^5.0.0 48 | 49 | flutter: 50 | uses-material-design: true 51 | assets: 52 | - assets/bg.jpg 53 | - assets/user.png 54 | 55 | dependency_overrides: 56 | rxdart_ext: 57 | git: 58 | url: https://github.com/hoc081098/rxdart_ext.git 59 | ref: 138c130651ed5b57127851cfc9817ad7377c806e 60 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /screenshots/Screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/screenshots/Screenshot1.png -------------------------------------------------------------------------------- /screenshots/Screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/screenshots/Screenshot2.png -------------------------------------------------------------------------------- /screenshots/Screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/screenshots/Screenshot3.png -------------------------------------------------------------------------------- /screenshots/Screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/screenshots/Screenshot4.png -------------------------------------------------------------------------------- /screenshots/Screenshot5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/screenshots/Screenshot5.png -------------------------------------------------------------------------------- /screenshots/Screenshot6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/screenshots/Screenshot6.png -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:node_auth/app.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(const MyApp()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('0'), findsOneWidget); 19 | expect(find.text('1'), findsNothing); 20 | 21 | // Tap the '+' icon and trigger a frame. 22 | await tester.tap(find.byIcon(Icons.add)); 23 | await tester.pump(); 24 | 25 | // Verify that our counter has incremented. 26 | expect(find.text('0'), findsNothing); 27 | expect(find.text('1'), findsOneWidget); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | node_auth 33 | 34 | 35 | 36 | 39 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node_auth", 3 | "short_name": "node_auth", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(node_auth LANGUAGES CXX) 3 | 4 | set(BINARY_NAME "node_auth") 5 | 6 | cmake_policy(SET CMP0063 NEW) 7 | 8 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 9 | 10 | # Configure build options. 11 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 12 | if(IS_MULTICONFIG) 13 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 14 | CACHE STRING "" FORCE) 15 | else() 16 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 17 | set(CMAKE_BUILD_TYPE "Debug" CACHE 18 | STRING "Flutter build mode" FORCE) 19 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 20 | "Debug" "Profile" "Release") 21 | endif() 22 | endif() 23 | 24 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 25 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 26 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 27 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 28 | 29 | # Use Unicode for all projects. 30 | add_definitions(-DUNICODE -D_UNICODE) 31 | 32 | # Compilation settings that should be applied to most targets. 33 | function(APPLY_STANDARD_SETTINGS TARGET) 34 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 35 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 36 | target_compile_options(${TARGET} PRIVATE /EHsc) 37 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 38 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 39 | endfunction() 40 | 41 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 42 | 43 | # Flutter library and tool build rules. 44 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 45 | 46 | # Application build 47 | add_subdirectory("runner") 48 | 49 | # Generated plugin build rules, which manage building the plugins and adding 50 | # them to the application. 51 | include(flutter/generated_plugins.cmake) 52 | 53 | 54 | # === Installation === 55 | # Support files are copied into place next to the executable, so that it can 56 | # run in place. This is done instead of making a separate bundle (as on Linux) 57 | # so that building and running from within Visual Studio will work. 58 | set(BUILD_BUNDLE_DIR "$") 59 | # Make the "install" step default, as it's required to run. 60 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 61 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 62 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 63 | endif() 64 | 65 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 66 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 67 | 68 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 69 | COMPONENT Runtime) 70 | 71 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 72 | COMPONENT Runtime) 73 | 74 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 75 | COMPONENT Runtime) 76 | 77 | if(PLUGIN_BUNDLED_LIBRARIES) 78 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 79 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 80 | COMPONENT Runtime) 81 | endif() 82 | 83 | # Fully re-copy the assets directory on each build to avoid having stale files 84 | # from a previous install. 85 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 86 | install(CODE " 87 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 88 | " COMPONENT Runtime) 89 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 90 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 91 | 92 | # Install the AOT library on non-Debug builds only. 93 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 94 | CONFIGURATIONS Profile;Release 95 | COMPONENT Runtime) 96 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | 3 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 4 | 5 | # Configuration provided via flutter tool. 6 | include(${EPHEMERAL_DIR}/generated_config.cmake) 7 | 8 | # TODO: Move the rest of this into files in ephemeral. See 9 | # https://github.com/flutter/flutter/issues/57146. 10 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 11 | 12 | # === Flutter Library === 13 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 14 | 15 | # Published to parent scope for install step. 16 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 17 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 18 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 19 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 20 | 21 | list(APPEND FLUTTER_LIBRARY_HEADERS 22 | "flutter_export.h" 23 | "flutter_windows.h" 24 | "flutter_messenger.h" 25 | "flutter_plugin_registrar.h" 26 | "flutter_texture_registrar.h" 27 | ) 28 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 29 | add_library(flutter INTERFACE) 30 | target_include_directories(flutter INTERFACE 31 | "${EPHEMERAL_DIR}" 32 | ) 33 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 34 | add_dependencies(flutter flutter_assemble) 35 | 36 | # === Wrapper === 37 | list(APPEND CPP_WRAPPER_SOURCES_CORE 38 | "core_implementations.cc" 39 | "standard_codec.cc" 40 | ) 41 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 42 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 43 | "plugin_registrar.cc" 44 | ) 45 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 46 | list(APPEND CPP_WRAPPER_SOURCES_APP 47 | "flutter_engine.cc" 48 | "flutter_view_controller.cc" 49 | ) 50 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 51 | 52 | # Wrapper sources needed for a plugin. 53 | add_library(flutter_wrapper_plugin STATIC 54 | ${CPP_WRAPPER_SOURCES_CORE} 55 | ${CPP_WRAPPER_SOURCES_PLUGIN} 56 | ) 57 | apply_standard_settings(flutter_wrapper_plugin) 58 | set_target_properties(flutter_wrapper_plugin PROPERTIES 59 | POSITION_INDEPENDENT_CODE ON) 60 | set_target_properties(flutter_wrapper_plugin PROPERTIES 61 | CXX_VISIBILITY_PRESET hidden) 62 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 63 | target_include_directories(flutter_wrapper_plugin PUBLIC 64 | "${WRAPPER_ROOT}/include" 65 | ) 66 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 67 | 68 | # Wrapper sources needed for the runner. 69 | add_library(flutter_wrapper_app STATIC 70 | ${CPP_WRAPPER_SOURCES_CORE} 71 | ${CPP_WRAPPER_SOURCES_APP} 72 | ) 73 | apply_standard_settings(flutter_wrapper_app) 74 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 75 | target_include_directories(flutter_wrapper_app PUBLIC 76 | "${WRAPPER_ROOT}/include" 77 | ) 78 | add_dependencies(flutter_wrapper_app flutter_assemble) 79 | 80 | # === Flutter tool backend === 81 | # _phony_ is a non-existent file to force this command to run every time, 82 | # since currently there's no way to get a full input/output list from the 83 | # flutter tool. 84 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 85 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 86 | add_custom_command( 87 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 88 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 89 | ${CPP_WRAPPER_SOURCES_APP} 90 | ${PHONY_OUTPUT} 91 | COMMAND ${CMAKE_COMMAND} -E env 92 | ${FLUTTER_TOOL_ENVIRONMENT} 93 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 94 | windows-x64 $ 95 | VERBATIM 96 | ) 97 | add_custom_target(flutter_assemble DEPENDS 98 | "${FLUTTER_LIBRARY}" 99 | ${FLUTTER_LIBRARY_HEADERS} 100 | ${CPP_WRAPPER_SOURCES_CORE} 101 | ${CPP_WRAPPER_SOURCES_PLUGIN} 102 | ${CPP_WRAPPER_SOURCES_APP} 103 | ) 104 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | 11 | void RegisterPlugins(flutter::PluginRegistry* registry) { 12 | FileSelectorWindowsRegisterWithRegistrar( 13 | registry->GetRegistrarForPlugin("FileSelectorWindows")); 14 | } 15 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | file_selector_windows 7 | ) 8 | 9 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 10 | ) 11 | 12 | set(PLUGIN_BUNDLED_LIBRARIES) 13 | 14 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 15 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 16 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 19 | endforeach(plugin) 20 | 21 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 24 | endforeach(ffi_plugin) 25 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | add_executable(${BINARY_NAME} WIN32 5 | "flutter_window.cpp" 6 | "main.cpp" 7 | "utils.cpp" 8 | "win32_window.cpp" 9 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 10 | "Runner.rc" 11 | "runner.exe.manifest" 12 | ) 13 | apply_standard_settings(${BINARY_NAME}) 14 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 15 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 16 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 17 | add_dependencies(${BINARY_NAME} flutter_assemble) 18 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #ifdef FLUTTER_BUILD_NUMBER 64 | #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0 67 | #endif 68 | 69 | #ifdef FLUTTER_BUILD_NAME 70 | #define VERSION_AS_STRING #FLUTTER_BUILD_NAME 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.hoc" "\0" 93 | VALUE "FileDescription", "node_auth" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "node_auth" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2022 com.hoc. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "node_auth.exe" "\0" 98 | VALUE "ProductName", "node_auth" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | return true; 30 | } 31 | 32 | void FlutterWindow::OnDestroy() { 33 | if (flutter_controller_) { 34 | flutter_controller_ = nullptr; 35 | } 36 | 37 | Win32Window::OnDestroy(); 38 | } 39 | 40 | LRESULT 41 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 42 | WPARAM const wparam, 43 | LPARAM const lparam) noexcept { 44 | // Give Flutter, including plugins, an opportunity to handle window messages. 45 | if (flutter_controller_) { 46 | std::optional result = 47 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 48 | lparam); 49 | if (result) { 50 | return *result; 51 | } 52 | } 53 | 54 | switch (message) { 55 | case WM_FONTCHANGE: 56 | flutter_controller_->engine()->ReloadSystemFonts(); 57 | break; 58 | } 59 | 60 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 61 | } 62 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.CreateAndShow(L"node_auth", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart/68edfb61af7d048063c0bf9d7e4780f15e8e9a68/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | if (target_length == 0) { 52 | return std::string(); 53 | } 54 | std::string utf8_string; 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates and shows a win32 window with |title| and position and size using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | --------------------------------------------------------------------------------