From bc51791e186f5d28d542b4d9c8426ede6328c9af Mon Sep 17 00:00:00 2001 From: Rodrigo Reis Date: Wed, 29 Oct 2025 10:52:02 -0700 Subject: [PATCH] SDKS-4217 Updated Plugin to support latest Flutter SDK --- README.md | 6 +- forgerock-authenticator/README.md | 6 +- forgerock-authenticator/android/build.gradle | 30 ++- .../gradle/wrapper/gradle-wrapper.properties | 3 +- .../android/proguard-rules.pro | 145 ++++++++++++++ .../android/src/main/AndroidManifest.xml | 3 +- .../example/android/app/build.gradle | 63 +++--- .../example/android/app/proguard-rules.pro | 189 ++++++++++++++++++ .../example/android/build.gradle | 19 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../example/android/settings.gradle | 32 ++- forgerock-authenticator/example/lib/main.dart | 15 +- .../lib/providers/authenticator_provider.dart | 78 ++++++-- .../example/lib/screens/accounts_screen.dart | 49 ++--- .../example/lib/screens/home_screen.dart | 87 ++++---- .../example/lib/widgets/account_box.dart | 18 +- .../example/lib/widgets/account_card.dart | 34 ++-- .../lib/widgets/account_circle_avatar.dart | 16 +- .../example/lib/widgets/account_detail.dart | 182 +++++++++-------- .../example/lib/widgets/account_list.dart | 40 ++-- .../lib/widgets/account_list_empty.dart | 32 +-- .../lib/widgets/account_list_tile.dart | 24 ++- .../example/lib/widgets/account_logo.dart | 14 +- .../example/lib/widgets/actions_menu.dart | 36 ++-- .../example/lib/widgets/alert_dialog.dart | 59 +++--- .../example/lib/widgets/app_bar.dart | 32 ++- .../example/lib/widgets/challenge_button.dart | 20 +- .../example/lib/widgets/count_down_timer.dart | 32 ++- .../example/lib/widgets/default_button.dart | 17 +- .../lib/widgets/delete_account_dialog.dart | 38 ++-- .../example/lib/widgets/notification_box.dart | 10 +- .../lib/widgets/notification_dialog.dart | 109 +++++----- forgerock-authenticator/example/pubspec.yaml | 2 +- .../ios/Classes/FRAClientWrapper.swift | 6 +- forgerock-authenticator/pubspec.yaml | 2 +- 35 files changed, 963 insertions(+), 487 deletions(-) create mode 100644 forgerock-authenticator/android/proguard-rules.pro create mode 100644 forgerock-authenticator/example/android/app/proguard-rules.pro diff --git a/README.md b/README.md index 92e91c4..66a7f3d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

- Logo + Logo

ForgeRock Flutter Plugins

@@ -33,7 +33,7 @@ These are the available plugins in this repository. * Access Management (AM) 6.5.2 * Android API level 23+ * iOS 12 and above -* Flutter SDK 2.10.x and above +* Flutter SDK 3.9.x and above ## Documentation @@ -66,4 +66,4 @@ If you would like to contribute to this project you can fork the repository, clo ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details \ No newline at end of file diff --git a/forgerock-authenticator/README.md b/forgerock-authenticator/README.md index c3f8233..d94ad39 100644 --- a/forgerock-authenticator/README.md +++ b/forgerock-authenticator/README.md @@ -4,7 +4,7 @@

- Logo + Logo

ForgeRock Authenticator Plugin

@@ -35,7 +35,7 @@ This project is provided as a Flutter plugin, a specialized package that include * Access Management (AM) 6.5.2 * Android API level 23+ * iOS 12 and above -* Flutter SDK 2.10.x and above +* Flutter SDK 3.9.x and above ## Installation @@ -89,4 +89,4 @@ If you would like to contribute to this project you can fork the repository, clo ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details \ No newline at end of file diff --git a/forgerock-authenticator/android/build.gradle b/forgerock-authenticator/android/build.gradle index b838ecf..9ba9e43 100644 --- a/forgerock-authenticator/android/build.gradle +++ b/forgerock-authenticator/android/build.gradle @@ -1,16 +1,17 @@ -group 'com.forgerock.authenticator' -version '1.0' +group 'org.forgerock.forgerock_authenticator' +version '1.0-SNAPSHOT' buildscript { + ext.kotlin_version = '2.1.0' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' - classpath 'com.google.gms:google-services:4.3.15' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0" + classpath 'com.android.tools.build:gradle:8.9.1' + classpath 'com.google.gms:google-services:4.4.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,19 +26,33 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 33 + namespace 'org.forgerock.forgerock_authenticator' + compileSdk 34 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = '1.8' + } + defaultConfig { minSdkVersion 23 + consumerProguardFiles 'proguard-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } } } dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.annotation:annotation:1.6.0' implementation 'androidx.activity:activity-ktx:1.7.2' implementation 'com.google.firebase:firebase-messaging:23.1.2' @@ -54,5 +69,4 @@ dependencies { // implementation 'com.squareup.okhttp3:logging-interceptor:4.3.1' // implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' -} - +} \ No newline at end of file diff --git a/forgerock-authenticator/android/gradle/wrapper/gradle-wrapper.properties b/forgerock-authenticator/android/gradle/wrapper/gradle-wrapper.properties index 3c472b9..b41f5f0 100644 --- a/forgerock-authenticator/android/gradle/wrapper/gradle-wrapper.properties +++ b/forgerock-authenticator/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Mon Oct 27 17:16:18 PDT 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/forgerock-authenticator/android/proguard-rules.pro b/forgerock-authenticator/android/proguard-rules.pro new file mode 100644 index 0000000..375a6d0 --- /dev/null +++ b/forgerock-authenticator/android/proguard-rules.pro @@ -0,0 +1,145 @@ +# =================================== +# Keep attributes for debugging and proper functionality +# =================================== +-keepattributes Signature +-keepattributes *Annotation* +-keepattributes SourceFile +-keepattributes LineNumberTable +-keepattributes EnclosingMethod +-keepattributes InnerClasses + +# =================================== +# Gson - JSON Serialization (CRITICAL) +# =================================== +# Keep Gson TypeToken classes (fixes the TypeToken crash) +-keep class com.google.gson.reflect.TypeToken { *; } +-keep class * extends com.google.gson.reflect.TypeToken + +# Keep generic signature of TypeToken (solves the TypeToken issue) +-keep class com.google.gson.internal.$Gson$Types { *; } +-keep class com.google.gson.internal.** { *; } + +# Gson specific classes +-dontwarn sun.misc.** +-keep class sun.misc.Unsafe { *; } + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# Retain generic signatures of TypeToken and its subclasses with members +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken + +# Keep generic type parameters for Gson +-keep class * implements com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Keep all classes that might be used with Gson reflection +-keepclassmembers class * { + @com.google.gson.annotations.SerializedName ; + @com.google.gson.annotations.Expose ; +} + +# =================================== +# ForgeRock SDK (CRITICAL) +# =================================== +# Keep all ForgeRock classes and their generic signatures +-keep class org.forgerock.** { *; } +-keep interface org.forgerock.** { *; } + +# Keep all model classes that might be serialized/deserialized +-keepclassmembers class org.forgerock.** { + ; + ; +} + +# =================================== +# OkHttp3 - HTTP Client +# =================================== +# JSR 305 annotations are for embedding nullability information. +-dontwarn javax.annotation.** + +# A resource is loaded with a relative path so the package of this class must be preserved. +-adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz + +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* + +# OkHttp platform used only on JVM and when Conscrypt and other security providers are available. +-dontwarn okhttp3.internal.platform.** +-dontwarn org.conscrypt.** +-dontwarn org.bouncycastle.** +-dontwarn org.openjsse.** + +# Keep OkHttp classes +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } +-dontwarn okhttp3.** + +# Keep Okio classes (OkHttp dependency) +-keep class okio.** { *; } +-dontwarn okio.** + +# =================================== +# Nimbus JOSE+JWT - JWT/JWE Processing +# =================================== +# Keep Nimbus classes for JWT handling +-keep class com.nimbusds.** { *; } +-keep interface com.nimbusds.** { *; } +-dontwarn com.nimbusds.** + +# Keep JWT and JWE classes +-keepclassmembers class com.nimbusds.jose.** { *; } +-keepclassmembers class com.nimbusds.jwt.** { *; } + +# Keep cryptography providers +-keep class org.bouncycastle.** { *; } +-dontwarn org.bouncycastle.** +-keep class javax.crypto.** { *; } + +# =================================== +# Kotlin and Coroutines +# =================================== +# Keep Kotlin metadata +-keep class kotlin.** { *; } +-keep class kotlin.Metadata { *; } +-dontwarn kotlin.** +-keepclassmembers class **$WhenMappings { + ; +} +-keepclassmembers class kotlin.Metadata { + public ; +} + +# Kotlinx Coroutines +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} +-keepclassmembers class kotlinx.coroutines.** { + volatile ; +} +-keepclassmembers class kotlin.coroutines.SafeContinuation { + volatile ; +} +-dontwarn kotlinx.coroutines.** + +# =================================== +# AndroidX and Support Libraries +# =================================== +-keep class androidx.** { *; } +-keep interface androidx.** { *; } +-dontwarn androidx.** + +# Keep AndroidX Biometric classes (if used) +-keep class androidx.biometric.** { *; } + +# =================================== +# Firebase and Google Play Services +# =================================== +-keep class com.google.firebase.** { *; } +-keep class com.google.android.gms.** { *; } +-dontwarn com.google.firebase.** +-dontwarn com.google.android.gms.** \ No newline at end of file diff --git a/forgerock-authenticator/android/src/main/AndroidManifest.xml b/forgerock-authenticator/android/src/main/AndroidManifest.xml index 8df42ae..29e50d2 100644 --- a/forgerock-authenticator/android/src/main/AndroidManifest.xml +++ b/forgerock-authenticator/android/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/forgerock-authenticator/example/android/app/build.gradle b/forgerock-authenticator/example/android/app/build.gradle index 9884833..6418315 100644 --- a/forgerock-authenticator/example/android/app/build.gradle +++ b/forgerock-authenticator/example/android/app/build.gradle @@ -1,29 +1,29 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'dev.flutter.flutter-gradle-plugin' + id 'com.google.gms.google-services' + id 'com.google.firebase.crashlytics' +} + def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') +def localPropertiesFile = rootProject.file("local.properties") if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> + localPropertiesFile.withReader("UTF-8") { reader -> localProperties.load(reader) } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") if (flutterVersionCode == null) { - flutterVersionCode = '1' + flutterVersionCode = "1" } -def flutterVersionName = localProperties.getProperty('flutter.versionName') +def flutterVersionName = localProperties.getProperty("flutter.versionName") if (flutterVersionName == null) { - flutterVersionName = '1.0' + flutterVersionName = "1.0" } -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - // The keystore.properties holds configuration for the release keystore def keystorePropertiesFile = rootProject.file("key.properties") def keystorePropertiesFileExists = keystorePropertiesFile.exists() @@ -34,12 +34,13 @@ if (keystorePropertiesFileExists) { } android { - compileSdkVersion 33 + namespace "com.example.authenticator" + compileSdk flutter.compileSdkVersion defaultConfig { applicationId "com.example.authenticator" - minSdkVersion 23 - targetSdkVersion 33 + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -60,16 +61,29 @@ android { buildTypes { debug { signingConfig signingConfigs.debug + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } release { if (keystorePropertiesFileExists) { signingConfig signingConfigs.release } + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - lintOptions { - abortOnError false + lint { + abortOnError = false + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() } } @@ -78,12 +92,9 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation 'com.google.firebase:firebase-messaging:23.1.1' -} - -apply plugin: 'com.google.gms.google-services' -apply plugin: 'com.google.firebase.crashlytics' +} \ No newline at end of file diff --git a/forgerock-authenticator/example/android/app/proguard-rules.pro b/forgerock-authenticator/example/android/app/proguard-rules.pro new file mode 100644 index 0000000..35c5036 --- /dev/null +++ b/forgerock-authenticator/example/android/app/proguard-rules.pro @@ -0,0 +1,189 @@ +# =================================== +# Keep attributes for debugging and proper functionality +# =================================== +-keepattributes Signature +-keepattributes *Annotation* +-keepattributes SourceFile +-keepattributes LineNumberTable +-keepattributes EnclosingMethod +-keepattributes InnerClasses + +# =================================== +# Gson - JSON Serialization (CRITICAL) +# =================================== +# Keep Gson TypeToken classes (fixes the TypeToken crash) +-keep class com.google.gson.reflect.TypeToken { *; } +-keep class * extends com.google.gson.reflect.TypeToken + +# Keep generic signature of TypeToken (solves the TypeToken issue) +-keep class com.google.gson.internal.$Gson$Types { *; } +-keep class com.google.gson.internal.** { *; } + +# Gson specific classes +-dontwarn sun.misc.** +-keep class sun.misc.Unsafe { *; } + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# Retain generic signatures of TypeToken and its subclasses with members +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken + +# Keep generic type parameters for Gson +-keep class * implements com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Keep all classes that might be used with Gson reflection +-keepclassmembers class * { + @com.google.gson.annotations.SerializedName ; + @com.google.gson.annotations.Expose ; +} + +# =================================== +# ForgeRock SDK (CRITICAL) +# =================================== +# Keep all ForgeRock classes and their generic signatures +-keep class org.forgerock.** { *; } +-keep interface org.forgerock.** { *; } + +# Keep all model classes that might be serialized/deserialized +-keepclassmembers class org.forgerock.** { + ; + ; +} + +# =================================== +# OkHttp3 - HTTP Client +# =================================== +# JSR 305 annotations are for embedding nullability information. +-dontwarn javax.annotation.** + +# A resource is loaded with a relative path so the package of this class must be preserved. +-adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz + +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* + +# OkHttp platform used only on JVM and when Conscrypt and other security providers are available. +-dontwarn okhttp3.internal.platform.** +-dontwarn org.conscrypt.** +-dontwarn org.bouncycastle.** +-dontwarn org.openjsse.** + +# Keep OkHttp classes +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } +-dontwarn okhttp3.** + +# Keep Okio classes (OkHttp dependency) +-keep class okio.** { *; } +-dontwarn okio.** + +# =================================== +# Nimbus JOSE+JWT - JWT/JWE Processing +# =================================== +# Keep Nimbus classes for JWT handling +-keep class com.nimbusds.** { *; } +-keep interface com.nimbusds.** { *; } +-dontwarn com.nimbusds.** + +# Keep JWT and JWE classes +-keepclassmembers class com.nimbusds.jose.** { *; } +-keepclassmembers class com.nimbusds.jwt.** { *; } + +# Keep cryptography providers +-keep class org.bouncycastle.** { *; } +-dontwarn org.bouncycastle.** +-keep class javax.crypto.** { *; } + +# =================================== +# Kotlin and Coroutines +# =================================== +# Keep Kotlin metadata +-keep class kotlin.** { *; } +-keep class kotlin.Metadata { *; } +-dontwarn kotlin.** +-keepclassmembers class **$WhenMappings { + ; +} +-keepclassmembers class kotlin.Metadata { + public ; +} + +# Kotlinx Coroutines +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} +-keepclassmembers class kotlinx.coroutines.** { + volatile ; +} +-keepclassmembers class kotlin.coroutines.SafeContinuation { + volatile ; +} +-dontwarn kotlinx.coroutines.** + +# =================================== +# AndroidX and Support Libraries +# =================================== +-keep class androidx.** { *; } +-keep interface androidx.** { *; } +-dontwarn androidx.** + +# Keep AndroidX Biometric classes (if used) +-keep class androidx.biometric.** { *; } + +# =================================== +# Firebase and Google Play Services +# =================================== +-keep class com.google.firebase.** { *; } +-keep class com.google.android.gms.** { *; } +-dontwarn com.google.firebase.** +-dontwarn com.google.android.gms.** + +# Keep Firebase Messaging classes +-keep class com.google.firebase.messaging.** { *; } +-keep class com.google.android.gms.common.** { *; } + +# =================================== +# Flutter +# =================================== +# Flutter wrapper +-keep class io.flutter.app.** { *; } +-keep class io.flutter.plugin.** { *; } +-keep class io.flutter.util.** { *; } +-keep class io.flutter.view.** { *; } +-keep class io.flutter.** { *; } +-keep class io.flutter.plugins.** { *; } +-keep class io.flutter.embedding.** { *; } + +# =================================== +# Android Components +# =================================== +# Keep broadcast receivers +-keep public class * extends android.content.BroadcastReceiver +-keep class org.forgerock.android.auth.FRAMessagingReceiver { *; } + +# Keep services +-keep public class * extends android.app.Service +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference + +# Parcelable +-keepclassmembers class * implements android.os.Parcelable { + public static final ** CREATOR; +} + +# Serializable +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} \ No newline at end of file diff --git a/forgerock-authenticator/example/android/build.gradle b/forgerock-authenticator/example/android/build.gradle index ad46f2a..323c3f3 100644 --- a/forgerock-authenticator/example/android/build.gradle +++ b/forgerock-authenticator/example/android/build.gradle @@ -1,18 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.0' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' - classpath 'com.google.gms:google-services:4.3.15' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -33,6 +18,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { - delete rootProject.buildDir +tasks.register("clean", Delete) { + delete rootProject.layout.buildDirectory } diff --git a/forgerock-authenticator/example/android/gradle/wrapper/gradle-wrapper.properties b/forgerock-authenticator/example/android/gradle/wrapper/gradle-wrapper.properties index 6b66533..1126aa0 100644 --- a/forgerock-authenticator/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/forgerock-authenticator/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/forgerock-authenticator/example/android/settings.gradle b/forgerock-authenticator/example/android/settings.gradle index 44e62bc..3d088c1 100644 --- a/forgerock-authenticator/example/android/settings.gradle +++ b/forgerock-authenticator/example/android/settings.gradle @@ -1,11 +1,27 @@ -include ':app' +pluginManagement { + def localPropertiesFile = new File(rootDir, "local.properties") + def properties = new Properties() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + assert localPropertiesFile.exists() + localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.9.1" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false + id "com.google.gms.google-services" version "4.4.2" apply false + id "com.google.firebase.crashlytics" version "3.0.2" apply false +} + +include ":app" diff --git a/forgerock-authenticator/example/lib/main.dart b/forgerock-authenticator/example/lib/main.dart index 556d782..9b19990 100644 --- a/forgerock-authenticator/example/lib/main.dart +++ b/forgerock-authenticator/example/lib/main.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -15,25 +15,26 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); // initialize SDK - AuthenticatorProvider.initialize(); + await AuthenticatorProvider.initialize(); - runApp(AuthenticatorApp()); + runApp(const AuthenticatorApp()); } class AuthenticatorApp extends StatelessWidget { + const AuthenticatorApp({super.key}); @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider( - create: (_) => AuthenticatorProvider()..getAllAccounts() + create: (_) => AuthenticatorProvider()..getAllAccounts(), ), ], child: MaterialApp( - home: HomeScreen(), + debugShowCheckedModeBanner: false, + home: const HomeScreen(), ), ); } - -} \ No newline at end of file +} diff --git a/forgerock-authenticator/example/lib/providers/authenticator_provider.dart b/forgerock-authenticator/example/lib/providers/authenticator_provider.dart index 14a46db..f6718b0 100644 --- a/forgerock-authenticator/example/lib/providers/authenticator_provider.dart +++ b/forgerock-authenticator/example/lib/providers/authenticator_provider.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -19,16 +19,19 @@ import 'package:forgerock_authenticator/models/push_notification.dart'; /// and operations required to manage OTP and Push accounts class AuthenticatorProvider with ChangeNotifier { List _accountList = []; - Map _accountIndex; + Map _accountIndex = {}; void updateAccountIndex() { _accountIndex = {}; for (final Account a in _accountList) { - _accountIndex[a.id] = a; + final String? accountId = a.id; + if (accountId != null && accountId.isNotEmpty) { + _accountIndex[accountId] = a; + } } } - Account getAccount(String accountId) { + Account? getAccount(String accountId) { return _accountIndex[accountId]; } @@ -36,7 +39,7 @@ class AuthenticatorProvider with ChangeNotifier { try { await ForgerockAuthenticator.start(); } on PlatformException catch (e) { - print(e); + debugPrint(e.toString()); } } @@ -55,19 +58,35 @@ class AuthenticatorProvider with ChangeNotifier { try { final Mechanism mechanism = await ForgerockAuthenticator.createMechanismFromUri(uri); - if (mechanism != null) { - getAllAccounts(); - } + await getAllAccounts(); return mechanism; } on PlatformException catch (e) { if (e.code == ForgerockAuthenticator.DuplicateMechanismException) { - return Future.error(DuplicateMechanismException(e.details, e.message)); + final String mechanismId = + e.details is String && (e.details as String).isNotEmpty + ? e.details as String + : 'unknown'; + return Future.error( + DuplicateMechanismException( + mechanismId, + e.message ?? 'This authentication method is already registered.', + ), + ); } if (e.code == ForgerockAuthenticator.CreateMechanismException) { return Future.error(MechanismCreationException(e.message)); } else if (e.code == ForgerockAuthenticator.PolicyViolationException) { + final String policyName = + e.details is String && (e.details as String).isNotEmpty + ? e.details as String + : 'unknown'; return Future.error( - PolicyViolationException(e.details, e.message)); + PolicyViolationException( + policyName, + e.message ?? + 'The account cannot be registered on this device. It violates some policy', + ), + ); } else { return Future.error(e); } @@ -75,14 +94,15 @@ class AuthenticatorProvider with ChangeNotifier { } Future removeAccount(String accountId) async { - final bool success = await ForgerockAuthenticator.removeAccount(accountId); + final bool success = + await ForgerockAuthenticator.removeAccount(accountId) ?? false; if (success) { - getAllAccounts(); + await getAllAccounts(); } return success; } - Future getOathTokenCode(String mechanismId) async { + Future getOathTokenCode(String mechanismId) async { try { return ForgerockAuthenticator.getOathTokenCode(mechanismId); } on PlatformException catch (e) { @@ -97,8 +117,13 @@ class AuthenticatorProvider with ChangeNotifier { static Future performPushAuthentication( PushNotification pushNotification, bool accept) async { try { - return ForgerockAuthenticator.performPushAuthentication( - pushNotification, accept); + final bool result = + await ForgerockAuthenticator.performPushAuthentication( + pushNotification, + accept, + ) ?? + false; + return result; } on PlatformException catch (e) { if (e.code == ForgerockAuthenticator.AccountLockException) { return Future.error(AccountLockException(e.message)); @@ -112,11 +137,17 @@ class AuthenticatorProvider with ChangeNotifier { static Future performPushAuthenticationWithChallenge( PushNotification pushNotification, - String challengeResponse, + String? challengeResponse, bool accept) async { try { - return ForgerockAuthenticator.performPushAuthenticationWithChallenge( - pushNotification, challengeResponse, accept); + final bool result = + await ForgerockAuthenticator.performPushAuthenticationWithChallenge( + pushNotification, + challengeResponse ?? '', + accept, + ) ?? + false; + return result; } on PlatformException catch (e) { if (e.code == ForgerockAuthenticator.AccountLockException) { return Future.error(AccountLockException(e.message)); @@ -134,8 +165,15 @@ class AuthenticatorProvider with ChangeNotifier { bool allowDeviceCredentials, bool accept) async { try { - return ForgerockAuthenticator.performPushAuthenticationWithBiometric( - pushNotification, title, allowDeviceCredentials, accept); + final bool result = + await ForgerockAuthenticator.performPushAuthenticationWithBiometric( + pushNotification, + title, + allowDeviceCredentials, + accept, + ) ?? + false; + return result; } on PlatformException catch (e) { if (e.code == ForgerockAuthenticator.AccountLockException) { return Future.error(AccountLockException(e.message)); diff --git a/forgerock-authenticator/example/lib/screens/accounts_screen.dart b/forgerock-authenticator/example/lib/screens/accounts_screen.dart index fabbcf3..1ef6c02 100644 --- a/forgerock-authenticator/example/lib/screens/accounts_screen.dart +++ b/forgerock-authenticator/example/lib/screens/accounts_screen.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 202-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -10,39 +10,42 @@ import 'package:provider/provider.dart'; import 'package:forgerock_authenticator_example/providers/authenticator_provider.dart'; import 'package:forgerock_authenticator_example/widgets/account_card.dart'; -import 'package:forgerock_authenticator_example/widgets/app_bar.dart'; import 'package:forgerock_authenticator_example/widgets/account_list_empty.dart'; +import 'package:forgerock_authenticator_example/widgets/app_bar.dart'; -/// This is the Accounts screen, which allows remove an account from the app. +/// This is the Accounts screen, which allows removing an account from the app. class AccountsScreen extends StatefulWidget { + const AccountsScreen({super.key}); @override - _AccountsScreenState createState() => _AccountsScreenState(); + State createState() => _AccountsScreenState(); } class _AccountsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AuthenticatorAppBar(), - body: Consumer( - builder: (context, authenticatorProvider, child) { - if (authenticatorProvider.accounts.isNotEmpty) { - return ListView( - children: List.generate(authenticatorProvider.accounts.length, (index) { - return AccountCard( - ValueKey(authenticatorProvider.accounts[index].id), - authenticatorProvider.accounts[index], - true - ); - }), + appBar: const AuthenticatorAppBar(), + body: Consumer( + builder: (BuildContext context, + AuthenticatorProvider authenticatorProvider, Widget? child) { + final accounts = authenticatorProvider.accounts; + if (accounts.isEmpty) { + return const AccountEmptyList(); + } + return ListView.builder( + itemCount: accounts.length, + itemBuilder: (BuildContext context, int index) { + final account = accounts[index]; + return AccountCard( + key: ValueKey(account.id ?? index), + account: account, + edit: true, ); - } else { - return AccountEmptyList(); - } - }, - ) + }, + ); + }, + ), ); } - -} \ No newline at end of file +} diff --git a/forgerock-authenticator/example/lib/screens/home_screen.dart b/forgerock-authenticator/example/lib/screens/home_screen.dart index c1922e8..6e5fc54 100644 --- a/forgerock-authenticator/example/lib/screens/home_screen.dart +++ b/forgerock-authenticator/example/lib/screens/home_screen.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -25,84 +25,92 @@ import '../widgets/notification_dialog.dart'; /// This is the main screen of the app. It shows a list of accounts registered /// with the SDK. class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { - final ForgerockPushConnector pushConnector = ForgerockPushConnector(); @override void initState() { - _setupPushConnector(); super.initState(); + _setupPushConnector(); } @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton( - onPressed: () { - _scan(context); - }, - child: Icon( + onPressed: () => _scan(context), + backgroundColor: Colors.orange, + child: const Icon( Icons.qr_code_scanner, size: 30, color: Colors.white, ), - backgroundColor: Colors.orange, - ), - appBar: AuthenticatorAppBar( - actions: [ - ActionsMenu() - ] ), - body: AccountList(), + appBar: const AuthenticatorAppBar(actions: [ActionsMenu()]), + body: const AccountList(), ); } - Future _setupPushConnector() async { + void _setupPushConnector() { pushConnector.token.addListener(() { - print('Token ${pushConnector.token.value}'); + final token = pushConnector.token.value; + if (token != null && token.isNotEmpty) { + debugPrint('Token $token'); + } }); pushConnector.pendingNotification.addListener(() { - _processPendingNotification(pushConnector.pendingNotification.value); + _processPendingNotification( + pushConnector.pendingNotification.value as String?, + ); }); } - Future _processPendingNotification(dynamic pendingNotification) async { - if(pendingNotification != null) { - final PushNotification notification = PushNotification.fromJson(jsonDecode(pendingNotification)); - if(notification.pending && !notification.isExpired()) { - print('Processing pending notification with id ${notification.messageId}'); - showDialog(context: context, builder: (BuildContext context) { - return NotificationDialog( - pushNotification: notification, - ); - }); + Future _processPendingNotification(String? pendingNotification) async { + if (pendingNotification == null || pendingNotification.isEmpty) { + return; + } + final PushNotification notification = + PushNotification.fromJson(jsonDecode(pendingNotification)); + if (notification.pending == true && !notification.isExpired()) { + debugPrint('Processing pending notification with id ${notification.messageId}'); + if (!mounted) { + return; } + await showDialog( + context: context, + builder: (BuildContext dialogContext) => NotificationDialog( + pushNotification: notification, + ), + ); } } Future _scan(BuildContext context) async { - final BuildContext rootContext = - context.findRootAncestorStateOfType().context; + final NavigatorState navigator = Navigator.of(context, rootNavigator: true); + final BuildContext rootContext = navigator.context; try { - ScanResult result = await BarcodeScanner.scan(); - String qrResult = result.rawContent; - if(qrResult != '') { - Provider.of(context, listen: false) - .addAccount(qrResult) - .catchError((Object error) { - alert(rootContext, 'Error adding account via QRCode', error.toString()); - }); + final ScanResult result = await BarcodeScanner.scan(); + final String qrResult = result.rawContent; + if (qrResult.isNotEmpty) { + final authenticatorProvider = + Provider.of(context, listen: false); + try { + await authenticatorProvider.addAccount(qrResult); + } catch (error) { + alert(rootContext, 'Error adding account via QRCode', + error.toString()); + } } } on PlatformException catch (e) { if (e.code == BarcodeScanner.cameraAccessDenied) { - alert(context, 'Error', 'Camera Access was not granted'); + alert(context, 'Error', 'Camera access was not granted'); } else { alert(context, 'Error', e.toString()); } @@ -110,5 +118,4 @@ class _HomeScreenState extends State { alert(context, 'Error', e.toString()); } } - -} \ No newline at end of file +} diff --git a/forgerock-authenticator/example/lib/widgets/account_box.dart b/forgerock-authenticator/example/lib/widgets/account_box.dart index 6026524..9a7c3b4 100644 --- a/forgerock-authenticator/example/lib/widgets/account_box.dart +++ b/forgerock-authenticator/example/lib/widgets/account_box.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -9,29 +9,29 @@ import 'package:flutter/material.dart'; /// This widget decorates the [AccountCard] with a BoxShadow. class AccountBox extends StatelessWidget { + const AccountBox({super.key, required this.child}); final Widget child; - const AccountBox({Key key, this.child}) : super(key: key); - @override Widget build(BuildContext context) { return Container( - margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - padding: EdgeInsets.all(12), + margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + padding: const EdgeInsets.all(12), width: double.infinity, decoration: BoxDecoration( color: Colors.white, boxShadow: [ - BoxShadow(color: Colors.grey[300], + BoxShadow( + color: Colors.grey.shade300, blurRadius: 30, - offset: Offset(0, 8)) + offset: const Offset(0, 8)) ], borderRadius: BorderRadius.circular(15.0), - border: Border.all(color: Color(0xFFEAEAEA), width: 1.0), + border: Border.all(color: const Color(0xFFEAEAEA), width: 1.0), ), child: child, ); } -} \ No newline at end of file +} diff --git a/forgerock-authenticator/example/lib/widgets/account_card.dart b/forgerock-authenticator/example/lib/widgets/account_card.dart index b79c055..4598375 100644 --- a/forgerock-authenticator/example/lib/widgets/account_card.dart +++ b/forgerock-authenticator/example/lib/widgets/account_card.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -17,21 +17,28 @@ import 'delete_account_dialog.dart'; /// The [AccountCard] widget reprentes an [Account] registered with the SDK. This /// sample does not cover an Account with both OATH and PUSH mechanisms. class AccountCard extends StatelessWidget { - final Key key; + const AccountCard({ + super.key, + required this.account, + required this.edit, + }); + final Account account; final bool edit; - AccountCard(this.key, this.account, this.edit); - @override Widget build(BuildContext context) { + final String issuer = + account.getIssuer() ?? account.issuer ?? 'Unknown issuer'; + final String accountName = + account.getAccountName() ?? account.accountName ?? 'Account'; return AccountListTile( leading: AccountLogo( - imageURL: account.imageURL, - textFallback: account.issuer, + imageURL: account.imageURL, + textFallback: issuer, ), - title: account.issuer, - subtitle: account.accountName, + title: issuer, + subtitle: accountName, trailing: edit ? _deleteButton(context) : _emptyContainer(), child: edit ? _emptyContainer() : _accountDetail(), ); @@ -58,18 +65,20 @@ class AccountCard extends StatelessWidget { } Widget _emptyContainer() { - return Container(height: 0, width: 0,); + return const SizedBox.shrink(); } Widget _accountDetail() { - if (account.lock) { + if (account.lock == true) { return SizedBox( width: 230, - child: Text('Your account is locked due the policy: ' + account.lockingPolicy, + child: Text( + 'Your account is locked due the policy: ${account.lockingPolicy ?? 'Unknown'}', maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, - style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), + style: + const TextStyle(color: Colors.red, fontWeight: FontWeight.bold), ), ); } else { @@ -81,4 +90,3 @@ class AccountCard extends StatelessWidget { } } - diff --git a/forgerock-authenticator/example/lib/widgets/account_circle_avatar.dart b/forgerock-authenticator/example/lib/widgets/account_circle_avatar.dart index 5ee05b9..a2a6909 100644 --- a/forgerock-authenticator/example/lib/widgets/account_circle_avatar.dart +++ b/forgerock-authenticator/example/lib/widgets/account_circle_avatar.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -9,23 +9,25 @@ import 'package:flutter/material.dart'; /// This widget creates a [CircleAvatar] to represent the [Account]. class AccountCircleAvatar extends StatelessWidget { + const AccountCircleAvatar({super.key, required this.text}); final String text; - const AccountCircleAvatar({Key key, this.text}) : super(key: key); - @override Widget build(BuildContext context) { + final String fallback = + text.trim().isNotEmpty ? text.trim().substring(0, 1).toUpperCase() : '?'; return CircleAvatar( backgroundColor: Colors.grey, - child: Text(text.substring(0,1), - style: TextStyle( + child: Text( + fallback, + style: const TextStyle( fontSize: 24, color: Colors.white, - fontWeight: FontWeight.bold + fontWeight: FontWeight.bold, ), ), ); } -} \ No newline at end of file +} diff --git a/forgerock-authenticator/example/lib/widgets/account_detail.dart b/forgerock-authenticator/example/lib/widgets/account_detail.dart index af8ce4f..8200f8f 100644 --- a/forgerock-authenticator/example/lib/widgets/account_detail.dart +++ b/forgerock-authenticator/example/lib/widgets/account_detail.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -8,26 +8,26 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:forgerock_authenticator/models/oath_token_code.dart'; import 'package:forgerock_authenticator/models/account.dart'; import 'package:forgerock_authenticator/models/oath_mechanism.dart'; +import 'package:forgerock_authenticator/models/oath_token_code.dart'; import 'package:forgerock_authenticator_example/providers/authenticator_provider.dart'; import 'count_down_timer.dart'; /// This widget is used with OATH accounts to display an [OathTokenCode]. class AccountDetail extends StatefulWidget { - final Account account; + const AccountDetail(this.account, {super.key}); - const AccountDetail(this.account); + final Account account; @override - _AccountDetailState createState() => _AccountDetailState(); + State createState() => _AccountDetailState(); } class _AccountDetailState extends State { - Future _oathTokenCode; - int _duration; + late Future _oathTokenCode; + int _duration = 0; Account get account => widget.account; @@ -35,102 +35,128 @@ class _AccountDetailState extends State { void initState() { super.initState(); _oathTokenCode = _getNextOathTokenCode(); - _duration = 0; } void refresh() { setState(() { + _duration = 0; _oathTokenCode = _getNextOathTokenCode(); }); } @override Widget build(BuildContext context) { - return FutureBuilder( - future: _oathTokenCode, - builder: (context, snapshot) { - if(snapshot.hasData) { - if(_duration == 0) { - _duration = _getDuration(snapshot.data); - } - return Row( + return FutureBuilder( + future: _oathTokenCode, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox( + width: 38, + height: 38, + child: Center( + child: SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + ); + } + + final OathTokenCode? token = snapshot.data; + if (token == null || token.code == null) { + return const SizedBox.shrink(); + } + + if (_duration == 0) { + _duration = _getDuration(token); + } + + return Row( + children: [ + Column( + children: [ + Text( + _formatCode(token), + style: const TextStyle(fontSize: 32, color: Colors.black), + ) + ], + ), + Column( children: [ - Column( - children: [ - Text( - _formartCode(snapshot.data), - style: TextStyle(fontSize: 32, color: Colors.black) - ) - ], - ), - Column( - children: [ - Container( - padding: const EdgeInsets.fromLTRB(10, 0, 0, 0), - child: _displayOTPAction(context, snapshot.data), - ) - ], + Container( + padding: const EdgeInsets.fromLTRB(10, 0, 0, 0), + child: _displayOtpAction(token), ) ], - ); - } else { - return Container(height: 0,width: 0,); - } - }); + ) + ], + ); + }, + ); } - Widget _displayOTPAction(BuildContext context, OathTokenCode oathTokenCode) { - if(oathTokenCode.oathType == TokenType.TOTP) { - return Container( - padding: const EdgeInsets.fromLTRB(6, 0, 0, 0), - child: CountDownTimer( - timeTotal: _getDuration(oathTokenCode), - timeRemaining: _getSecondsLeft(oathTokenCode), - width: 38, - height: 38, - onComplete: refresh - ) - ); - } else { - return SizedBox( - height: 38.0, - width: 38.0, - child: IconButton( - padding: const EdgeInsets.all(0.0), - color: Colors.grey, - icon: const Icon(Icons.refresh, size: 42.0), - onPressed: refresh, - ) + Widget _displayOtpAction(OathTokenCode token) { + if (token.oathType == TokenType.TOTP) { + return CountDownTimer( + timeTotal: _getDuration(token), + timeRemaining: _getSecondsLeft(token), + width: 38, + height: 38, + onComplete: refresh, ); } + return SizedBox( + height: 38.0, + width: 38.0, + child: IconButton( + padding: const EdgeInsets.all(0.0), + color: Colors.grey, + icon: const Icon(Icons.refresh, size: 42.0), + onPressed: refresh, + ), + ); } - Future _getNextOathTokenCode() async { - return await Provider.of(context, listen: false) - .getOathTokenCode(account.getOathMechanism().id); + Future _getNextOathTokenCode() async { + final OathMechanism? mechanism = account.getOathMechanism(); + final String? mechanismId = mechanism?.id; + if (mechanismId == null) { + return null; + } + return Provider.of(context, listen: false) + .getOathTokenCode(mechanismId); } - String _formartCode(OathTokenCode oathTokenCode) { - String code = oathTokenCode.code; - int half = (code.length ~/ 2); - return code.substring(0, half) + " " + code.substring(half, code.length); + String _formatCode(OathTokenCode token) { + final String code = token.code ?? ''; + if (code.length <= 3) { + return code; + } + final int half = code.length ~/ 2; + return '${code.substring(0, half)} ${code.substring(half)}'; } - int _getSecondsLeft(OathTokenCode oathTokenCode) { - int cur = DateTime.now().millisecondsSinceEpoch; - int total = oathTokenCode.until - oathTokenCode.start; - int state = cur - oathTokenCode.start; - int secondsLeft = ((total - state) ~/ 1000)+1; - - if(0 <= secondsLeft) { - return secondsLeft; - } else { - return 1; + int _getSecondsLeft(OathTokenCode token) { + final int? until = token.until; + final int? start = token.start; + if (until == null || start == null) { + return 0; } - } + final int cur = DateTime.now().millisecondsSinceEpoch; + final int total = until - start; + final int state = cur - start; + final int secondsLeft = ((total - state) ~/ 1000) + 1; - int _getDuration(OathTokenCode oathTokenCode) { - return (oathTokenCode.until - oathTokenCode.start) ~/ 1000; + return secondsLeft >= 0 ? secondsLeft : 1; } + int _getDuration(OathTokenCode token) { + final int? until = token.until; + final int? start = token.start; + if (until == null || start == null) { + return 0; + } + return (until - start) ~/ 1000; + } } diff --git a/forgerock-authenticator/example/lib/widgets/account_list.dart b/forgerock-authenticator/example/lib/widgets/account_list.dart index 2e7f7da..6a451be 100644 --- a/forgerock-authenticator/example/lib/widgets/account_list.dart +++ b/forgerock-authenticator/example/lib/widgets/account_list.dart @@ -1,41 +1,43 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import 'package:flutter/material.dart'; -import 'package:forgerock_authenticator_example/widgets/account_list_empty.dart'; import 'package:provider/provider.dart'; import 'package:forgerock_authenticator_example/providers/authenticator_provider.dart'; +import 'package:forgerock_authenticator_example/widgets/account_list_empty.dart'; import 'account_card.dart'; -/// The [AccountList] widget list all accounts registered with the SDK. +/// The [AccountList] widget lists all accounts registered with the SDK. class AccountList extends StatelessWidget { + const AccountList({super.key}); @override Widget build(BuildContext context) { return Consumer( - builder: (context, authenticatorProvider, child) { - if (authenticatorProvider.accounts.isNotEmpty) { - return ListView.builder( - itemCount: authenticatorProvider.accounts.length, - itemBuilder: (context, index) { - return AccountCard( - ValueKey(authenticatorProvider.accounts[index].id), - authenticatorProvider.accounts[index], - false - ); - }, - ); - } else { - return AccountEmptyList(); + builder: (BuildContext context, + AuthenticatorProvider authenticatorProvider, Widget? child) { + final accounts = authenticatorProvider.accounts; + if (accounts.isEmpty) { + return const AccountEmptyList(); } - } + return ListView.builder( + itemCount: accounts.length, + itemBuilder: (BuildContext context, int index) { + final account = accounts[index]; + return AccountCard( + key: ValueKey(account.id ?? index), + account: account, + edit: false, + ); + }, + ); + }, ); } - } diff --git a/forgerock-authenticator/example/lib/widgets/account_list_empty.dart b/forgerock-authenticator/example/lib/widgets/account_list_empty.dart index 6ec405f..80623e8 100644 --- a/forgerock-authenticator/example/lib/widgets/account_list_empty.dart +++ b/forgerock-authenticator/example/lib/widgets/account_list_empty.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -9,45 +9,50 @@ import 'package:flutter/material.dart'; /// This widget is used when the account list is empty. class AccountEmptyList extends StatelessWidget { + const AccountEmptyList({super.key}); + @override Widget build(BuildContext context) { return Container( color: Colors.white, - padding: EdgeInsets.symmetric(vertical: 20.0), + padding: const EdgeInsets.symmetric(vertical: 20.0), child: Column( children: [ Flexible( flex: 1, fit: FlexFit.tight, child: Icon( - Icons.account_circle, - color: Colors.grey[300], - size: 200, + Icons.account_circle, + color: Colors.grey.shade300, + size: 200, ), ), Flexible( flex: 2, fit: FlexFit.tight, child: Container( - padding: EdgeInsets.symmetric(horizontal: 30.0), + padding: const EdgeInsets.symmetric(horizontal: 30.0), child: Column( - children: [ + children: const [ Text( 'No accounts!', - key: const Key('header'), + key: Key('header'), style: TextStyle( fontSize: 46.0, fontWeight: FontWeight.w300, color: Color(0XFF3F3D56), - height: 2.0)), + height: 2.0, + ), + ), Text( 'Register your first account using the camera to scan a QRCode', - key: const Key('description'), + key: Key('description'), style: TextStyle( color: Colors.grey, letterSpacing: 1.2, fontSize: 16.0, - height: 1.3), + height: 1.3, + ), textAlign: TextAlign.center, ) ], @@ -55,8 +60,7 @@ class AccountEmptyList extends StatelessWidget { ), ) ], - ) + ), ); } - -} \ No newline at end of file +} diff --git a/forgerock-authenticator/example/lib/widgets/account_list_tile.dart b/forgerock-authenticator/example/lib/widgets/account_list_tile.dart index 167f3ab..0ab516f 100644 --- a/forgerock-authenticator/example/lib/widgets/account_list_tile.dart +++ b/forgerock-authenticator/example/lib/widgets/account_list_tile.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -11,15 +11,21 @@ import 'account_box.dart'; /// The [AccountListTile] widget contains details of the [Account]. class AccountListTile extends StatelessWidget { + const AccountListTile({ + super.key, + required this.leading, + required this.title, + required this.subtitle, + this.trailing, + required this.child, + }); final Widget leading; final String title; final String subtitle; - final Widget trailing; + final Widget? trailing; final Widget child; - const AccountListTile({Key key, this.leading, this.title, this.subtitle, this.trailing, this.child}) : super(key: key); - @override Widget build(BuildContext context) { return AccountBox( @@ -34,7 +40,7 @@ class AccountListTile extends StatelessWidget { alignment: Alignment.topCenter, width: 80, height: 50, - padding: EdgeInsets.symmetric(horizontal: 5.0), + padding: const EdgeInsets.symmetric(horizontal: 5.0), child: leading ), ], @@ -47,14 +53,14 @@ class AccountListTile extends StatelessWidget { padding: const EdgeInsets.fromLTRB(5, 0, 0, 0), child: Text( title, - style: TextStyle(fontSize: 20), + style: const TextStyle(fontSize: 20), ) ), Container( padding: const EdgeInsets.fromLTRB(5, 0, 0, 0), child: Text( subtitle, - style: TextStyle(fontSize: 17), + style: const TextStyle(fontSize: 17), overflow: TextOverflow.ellipsis, maxLines: 1, softWrap: false, @@ -62,7 +68,7 @@ class AccountListTile extends StatelessWidget { ), ], )), - trailing, + trailing ?? const SizedBox.shrink(), ] ), Row( @@ -82,4 +88,4 @@ class AccountListTile extends StatelessWidget { ); } -} \ No newline at end of file +} diff --git a/forgerock-authenticator/example/lib/widgets/account_logo.dart b/forgerock-authenticator/example/lib/widgets/account_logo.dart index 1fe81b4..26b02ba 100644 --- a/forgerock-authenticator/example/lib/widgets/account_logo.dart +++ b/forgerock-authenticator/example/lib/widgets/account_logo.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -12,19 +12,19 @@ import 'account_circle_avatar.dart'; /// The [AccountLogo] widget displays the logo associated with an [Account]. If /// this is not available it creates a [AccountCircleAvatar]. class AccountLogo extends StatelessWidget { + const AccountLogo({super.key, this.imageURL, required this.textFallback}); - final String imageURL; + final String? imageURL; final String textFallback; - const AccountLogo({Key key, this.imageURL, this.textFallback}) : super(key: key); - @override Widget build(BuildContext context) { - if(imageURL == null || imageURL.trim().isEmpty) { + final String trimmed = imageURL?.trim() ?? ''; + if (trimmed.isEmpty) { return AccountCircleAvatar(text: textFallback); } else { return Image.network( - imageURL, + trimmed, fit: BoxFit.fill, alignment: Alignment.topCenter, errorBuilder: (context, error, stackTrace) { @@ -34,4 +34,4 @@ class AccountLogo extends StatelessWidget { } } -} \ No newline at end of file +} diff --git a/forgerock-authenticator/example/lib/widgets/actions_menu.dart b/forgerock-authenticator/example/lib/widgets/actions_menu.dart index 9d41fb2..50cecce 100644 --- a/forgerock-authenticator/example/lib/widgets/actions_menu.dart +++ b/forgerock-authenticator/example/lib/widgets/actions_menu.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -11,32 +11,32 @@ import 'package:forgerock_authenticator_example/screens/accounts_screen.dart'; /// The [ActionsMenu] widget contains the actions available on the [AppBar]. class ActionsMenu extends StatelessWidget { + const ActionsMenu({super.key}); @override Widget build(BuildContext context) { - return PopupMenuButton( - onSelected: (selectedValue) { - switch(selectedValue) { - case 'edit': { + return PopupMenuButton( + onSelected: (String selectedValue) { + switch (selectedValue) { + case 'edit': Navigator.push( context, - MaterialPageRoute(builder: (context) => AccountsScreen()), + MaterialPageRoute( + builder: (BuildContext context) => const AccountsScreen(), + ), ); - } - break; + break; } }, - itemBuilder: (BuildContext ctx) => - [ - PopupMenuItem( - child: ListTile( - leading: Icon(Icons.edit), - title: Text('Edit Accounts'), - ), - value: 'edit' + itemBuilder: (BuildContext ctx) => const >[ + PopupMenuItem( + value: 'edit', + child: ListTile( + leading: Icon(Icons.edit), + title: Text('Edit Accounts'), + ), ), - ] + ], ); } - } diff --git a/forgerock-authenticator/example/lib/widgets/alert_dialog.dart b/forgerock-authenticator/example/lib/widgets/alert_dialog.dart index d358c71..b721874 100644 --- a/forgerock-authenticator/example/lib/widgets/alert_dialog.dart +++ b/forgerock-authenticator/example/lib/widgets/alert_dialog.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,35 +7,32 @@ import 'package:flutter/material.dart'; -/// Displays an modal with the supplied title and message. -Future alert(BuildContext context, String title, String message) => - showDialog( - context: context, - builder: (var context) => AlertDialog( - title: Text( - title, - style: TextStyle( - color: Colors.black, - ), +/// Displays a modal dialog with the supplied title and message. +Future alert(BuildContext context, String title, String message) => + showDialog( + context: context, + builder: (BuildContext dialogContext) => AlertDialog( + title: Text( + title, + style: const TextStyle(color: Colors.black), + ), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text( + message, + style: const TextStyle(color: Colors.black), + ), + ], ), - content: SingleChildScrollView( - child: ListBody( - children: [ - Text( - message, - style: TextStyle( - color: Colors.black, - ), - ), - ], - ), + ), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.of(dialogContext).pop(); + }, ), - actions: [ - TextButton( - child: Text('OK'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - )); \ No newline at end of file + ], + ), + ); diff --git a/forgerock-authenticator/example/lib/widgets/app_bar.dart b/forgerock-authenticator/example/lib/widgets/app_bar.dart index dd0cd59..07881a4 100644 --- a/forgerock-authenticator/example/lib/widgets/app_bar.dart +++ b/forgerock-authenticator/example/lib/widgets/app_bar.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -9,26 +9,24 @@ import 'package:flutter/material.dart'; /// This widget represents the bar on the top of the application. It also contains /// an [ActionMenu]. -class AuthenticatorAppBar extends StatelessWidget implements PreferredSizeWidget{ - final List actions; +class AuthenticatorAppBar extends StatelessWidget implements PreferredSizeWidget { + const AuthenticatorAppBar({super.key, this.actions}); - AuthenticatorAppBar({Key key, this.actions}) : super(key: key); + final List? actions; @override Widget build(BuildContext context) { return AppBar( - backgroundColor: Color(0xff006ac8), - title: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Icon( - Icons.lock, - size: 24, - color: Colors.white, - ), - Container(width: 5), - Text('Authenticator') - ]), + backgroundColor: const Color(0xff006ac8), + title: Row(mainAxisAlignment: MainAxisAlignment.start, children: const [ + Icon( + Icons.lock, + size: 24, + color: Colors.white, + ), + SizedBox(width: 5), + Text('Authenticator') + ]), centerTitle: false, actions: actions, ); @@ -37,4 +35,4 @@ class AuthenticatorAppBar extends StatelessWidget implements PreferredSizeWidget @override Size get preferredSize => const Size.fromHeight(58); -} \ No newline at end of file +} diff --git a/forgerock-authenticator/example/lib/widgets/challenge_button.dart b/forgerock-authenticator/example/lib/widgets/challenge_button.dart index ec4bb16..5c8a02d 100644 --- a/forgerock-authenticator/example/lib/widgets/challenge_button.dart +++ b/forgerock-authenticator/example/lib/widgets/challenge_button.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,24 +7,26 @@ import 'package:flutter/material.dart'; - class ChallengeButton extends StatelessWidget { + const ChallengeButton({ + super.key, + required this.action, + required this.text, + }); final VoidCallback action; final String text; - const ChallengeButton({Key key, this.action, this.text}) : super(key: key); - @override Widget build(BuildContext context) { return Padding( - padding: EdgeInsets.fromLTRB(15, 5, 15, 5), + padding: const EdgeInsets.fromLTRB(15, 5, 15, 5), child: ElevatedButton( key: key, style: ElevatedButton.styleFrom( - primary: Colors.grey, - onPrimary: Colors.white, - shape: CircleBorder(), + foregroundColor: Colors.white, + backgroundColor: Colors.grey, + shape: const CircleBorder(), minimumSize: const Size(65, 65), ), onPressed: action, @@ -36,4 +38,4 @@ class ChallengeButton extends StatelessWidget { ); } -} \ No newline at end of file +} diff --git a/forgerock-authenticator/example/lib/widgets/count_down_timer.dart b/forgerock-authenticator/example/lib/widgets/count_down_timer.dart index de9d137..1f9c930 100644 --- a/forgerock-authenticator/example/lib/widgets/count_down_timer.dart +++ b/forgerock-authenticator/example/lib/widgets/count_down_timer.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -12,14 +12,14 @@ import 'package:flutter/material.dart'; /// This widget creates a circular container with a countdown with duration in /// seconds. class CountDownTimer extends StatefulWidget { - const CountDownTimer({ - Key key, - this.timeRemaining, - this.onComplete, - this.width, - this.height, - this.timeTotal}) : super(key: key); + super.key, + required this.timeRemaining, + required this.onComplete, + required this.width, + required this.height, + required this.timeTotal, + }); final int timeTotal; final int timeRemaining; @@ -32,8 +32,8 @@ class CountDownTimer extends StatefulWidget { } class CountDownTimerState extends State { - Timer _timer; - int _start; + Timer? _timer; + late int _start; @override void initState() { @@ -44,7 +44,7 @@ class CountDownTimerState extends State { @override void dispose() { - _timer.cancel(); + _timer?.cancel(); super.dispose(); } @@ -57,11 +57,11 @@ class CountDownTimerState extends State { decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, - border: Border.all(width: 4, color: Colors.grey[400]) + border: Border.all(width: 4, color: Colors.grey.shade400), ), child: Center(child: Text( '$_start', - textScaleFactor: 1.0, + textScaler: const TextScaler.linear(1.0), style: TextStyle( fontSize: 15.0, color: _start > (widget.timeRemaining / 3) @@ -73,10 +73,8 @@ class CountDownTimerState extends State { } void _onComplete() { - if (widget.onComplete != null) { - widget.onComplete(); - } - setState(() { + widget.onComplete(); + setState(() { _start = widget.timeTotal; _startTimer(); }); diff --git a/forgerock-authenticator/example/lib/widgets/default_button.dart b/forgerock-authenticator/example/lib/widgets/default_button.dart index 2eb8b7c..1065911 100644 --- a/forgerock-authenticator/example/lib/widgets/default_button.dart +++ b/forgerock-authenticator/example/lib/widgets/default_button.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,22 +7,25 @@ import 'package:flutter/material.dart'; - class DefaultButton extends StatelessWidget { + const DefaultButton({ + super.key, + required this.action, + required this.text, + required this.color, + }); final VoidCallback action; final String text; final Color color; - const DefaultButton({Key key, this.action, this.text, this.color}) : super(key: key); - @override Widget build(BuildContext context) { return ElevatedButton( key: key, style: ElevatedButton.styleFrom( - primary: color, - onPrimary: Colors.white, + foregroundColor: Colors.white, + backgroundColor: color, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(32.0)), minimumSize: const Size(150, 50), @@ -35,4 +38,4 @@ class DefaultButton extends StatelessWidget { ); } -} \ No newline at end of file +} diff --git a/forgerock-authenticator/example/lib/widgets/delete_account_dialog.dart b/forgerock-authenticator/example/lib/widgets/delete_account_dialog.dart index 04687b4..d42d3cc 100644 --- a/forgerock-authenticator/example/lib/widgets/delete_account_dialog.dart +++ b/forgerock-authenticator/example/lib/widgets/delete_account_dialog.dart @@ -1,36 +1,44 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/authenticator_provider.dart'; -/// Displays an modal to confirm the [Account] deletion. -void deleteAccount(BuildContext context, String id) { - showDialog( +/// Displays a modal to confirm the [Account] deletion. +Future deleteAccount(BuildContext context, String? id) async { + if (id == null || id.isEmpty) { + return; + } + await showDialog( context: context, - builder: (BuildContext context) { + builder: (BuildContext dialogContext) { return AlertDialog( - title: Text("Remove Account"), - content: Text("Are you sure you want to remove this account? If you " - "choose to continue, keep in mind that you may not be able to access" - " the system associated with this account."), + title: const Text('Remove Account'), + content: const Text( + 'Are you sure you want to remove this account? If you choose to continue, ' + 'keep in mind that you may not be able to access the system associated with this account.', + ), actions: [ TextButton( - child: Text("Cancel"), - onPressed: () { Navigator.of(context).pop(); }, + child: const Text('Cancel'), + onPressed: () { + Navigator.of(dialogContext).pop(); + }, ), TextButton( - child: Text("Continue"), - onPressed: () { - Provider.of(context, listen: false) + child: const Text('Continue'), + onPressed: () async { + await Provider.of(dialogContext, + listen: false) .removeAccount(id); - Navigator.of(context).pop(); + Navigator.of(dialogContext).pop(); }, ), ], diff --git a/forgerock-authenticator/example/lib/widgets/notification_box.dart b/forgerock-authenticator/example/lib/widgets/notification_box.dart index c11c570..c28047c 100644 --- a/forgerock-authenticator/example/lib/widgets/notification_box.dart +++ b/forgerock-authenticator/example/lib/widgets/notification_box.dart @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -9,11 +9,10 @@ import 'package:flutter/material.dart'; /// This widget is used to decorate a notification dialog. class NotificationBox extends StatelessWidget { + const NotificationBox({super.key, required this.child}); final Widget child; - const NotificationBox({Key key, this.child}) : super(key: key); - @override Widget build(BuildContext context) { return Dialog( @@ -32,8 +31,9 @@ class NotificationBox extends StatelessWidget { borderRadius: BorderRadius.circular(20), boxShadow: const [ BoxShadow( - color: Colors.black,offset: Offset(0,5), - blurRadius: 5 + color: Colors.black, + offset: Offset(0, 5), + blurRadius: 5, ), ] ), diff --git a/forgerock-authenticator/example/lib/widgets/notification_dialog.dart b/forgerock-authenticator/example/lib/widgets/notification_dialog.dart index f8649b0..2572737 100644 --- a/forgerock-authenticator/example/lib/widgets/notification_dialog.dart +++ b/forgerock-authenticator/example/lib/widgets/notification_dialog.dart @@ -1,12 +1,11 @@ /* - * Copyright (c) 2022-2023 ForgeRock. All rights reserved. + * Copyright (c) 2022-2025 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import 'package:flutter/material.dart'; -import 'package:forgerock_authenticator/exception/exceptions.dart'; import 'package:forgerock_authenticator/models/push_notification.dart'; import 'package:forgerock_authenticator/models/push_type.dart'; @@ -19,8 +18,7 @@ import 'package:forgerock_authenticator_example/providers/authenticator_provider /// This widget is used inside an modal dialog to display a [PushNotification] /// received by the app. class NotificationDialog extends StatelessWidget { - - const NotificationDialog({Key key, this.pushNotification}) : super(key: key); + const NotificationDialog({super.key, required this.pushNotification}); final PushNotification pushNotification; @@ -31,13 +29,16 @@ class NotificationDialog extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Push Authentication request', - style: const TextStyle(fontSize: 18,fontWeight: FontWeight.w600),textAlign: TextAlign.center, + const Text( + 'Push Authentication request', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + textAlign: TextAlign.center, ), - PushType.CHALLENGE.isEqual(pushNotification.pushType) - && pushNotification.getNumbersChallenge().isNotEmpty - ? _challengeButtons(context) - : _defaultButtons(context) + if ((pushNotification.pushType?.isEqual(PushType.CHALLENGE) ?? false) && + (pushNotification.getNumbersChallenge()?.isNotEmpty ?? false)) + _challengeButtons(context) + else + _defaultButtons(context) ]) ); } @@ -46,10 +47,11 @@ class NotificationDialog extends StatelessWidget { return Column( children: [ const SizedBox(height: 30), - Text(pushNotification.message != null - ? pushNotification.message - : 'Do you wish to accept sign in from another device?', - style: const TextStyle(fontSize: 16),textAlign: TextAlign.center, + Text( + pushNotification.message ?? + 'Do you wish to accept sign in from another device?', + style: const TextStyle(fontSize: 16), + textAlign: TextAlign.center, ), const SizedBox(height: 30), Row( @@ -60,7 +62,7 @@ class NotificationDialog extends StatelessWidget { action: () async { await _approve(true, context); }, - color: Color(0xff006ac8), + color: const Color(0xff006ac8), text: 'Accept', ), ], @@ -84,12 +86,18 @@ class NotificationDialog extends StatelessWidget { } Widget _challengeButtons(BuildContext context) { - List challenge = pushNotification.getNumbersChallenge(); + final List challenge = + pushNotification.getNumbersChallenge() ?? const []; + if (challenge.length < 3) { + return const SizedBox.shrink(); + } return Column( children: [ const SizedBox(height: 30), - Text('To continue with the Sign in, select the number you see on your Other screen', - style: const TextStyle(fontSize: 16),textAlign: TextAlign.center, + const Text( + 'To continue with the Sign in, select the number you see on your other screen', + style: TextStyle(fontSize: 16), + textAlign: TextAlign.center, ), const SizedBox(height: 30), Row( @@ -135,43 +143,48 @@ class NotificationDialog extends StatelessWidget { } Future _approve(bool approve, BuildContext context) async { - final BuildContext rootContext = - context.findRootAncestorStateOfType().context; + final NavigatorState navigator = + Navigator.of(context, rootNavigator: true); + final BuildContext rootContext = navigator.context; String message = 'Push Notification successfully processed.'; - if(pushNotification.pushType.isEqual(PushType.BIOMETRIC)) { - await AuthenticatorProvider.performPushAuthenticationWithBiometric( - pushNotification, 'Biometric is required to process this notification', true, approve - ).catchError((Object error) { - message = error.toString(); - }).then((Object value) { - _showResult(rootContext, message); - Navigator.of(rootContext).pop(); - }); - } else { - await AuthenticatorProvider.performPushAuthentication( - pushNotification, approve - ).catchError((Object error) { - message = error.toString(); - }).then((Object value) { - _showResult(rootContext, message); - Navigator.of(rootContext).pop(); - }); + try { + if (pushNotification.pushType?.isEqual(PushType.BIOMETRIC) ?? false) { + await AuthenticatorProvider.performPushAuthenticationWithBiometric( + pushNotification, + 'Biometric is required to process this notification', + true, + approve, + ); + } else { + await AuthenticatorProvider.performPushAuthentication( + pushNotification, + approve, + ); + } + } catch (error) { + message = error.toString(); } + _showResult(rootContext, message); + Navigator.of(rootContext).pop(); } Future _approveWithChallenge( - bool approve, String challenge, BuildContext context) async { - final BuildContext rootContext = - context.findRootAncestorStateOfType().context; + bool approve, String? challenge, BuildContext context) async { + final NavigatorState navigator = + Navigator.of(context, rootNavigator: true); + final BuildContext rootContext = navigator.context; String message = 'Push Notification successfully processed.'; - await AuthenticatorProvider.performPushAuthenticationWithChallenge( - pushNotification, challenge, approve - ).catchError((Object error) { + try { + await AuthenticatorProvider.performPushAuthenticationWithChallenge( + pushNotification, + challenge, + approve, + ); + } catch (error) { message = error.toString(); - }).then((Object value) { - _showResult(rootContext, message); - Navigator.of(rootContext).pop(); - }); + } + _showResult(rootContext, message); + Navigator.of(rootContext).pop(); } void _showResult(BuildContext context, String message) { diff --git a/forgerock-authenticator/example/pubspec.yaml b/forgerock-authenticator/example/pubspec.yaml index 82d676d..50dcec4 100644 --- a/forgerock-authenticator/example/pubspec.yaml +++ b/forgerock-authenticator/example/pubspec.yaml @@ -7,7 +7,7 @@ version: 1.2.0 publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - sdk: ">=2.7.0 <3.0.0" + sdk: '>=3.9.0 <4.0.0' dependencies: flutter: diff --git a/forgerock-authenticator/ios/Classes/FRAClientWrapper.swift b/forgerock-authenticator/ios/Classes/FRAClientWrapper.swift index d504a49..62e1742 100644 --- a/forgerock-authenticator/ios/Classes/FRAClientWrapper.swift +++ b/forgerock-authenticator/ios/Classes/FRAClientWrapper.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022-2023 ForgeRock. All rights reserved. +// Copyright (c) 2022-2025 ForgeRock. All rights reserved. // // This software may be modified and distributed under the terms // of the MIT license. See the LICENSE file for details. @@ -189,8 +189,7 @@ open class FRAClientWrapper { NSLog("Looking for: \(messageId)") let notificationList = FRAClient.shared?.getAllNotifications() for notification in notificationList! { - let jsonDictionary = ConverterUtil.convertStringToDictionary(jsonString: notification.toJson()!) - let mId = jsonDictionary?["messageId"] as! String + let mId = notification.messageId if(mId == messageId) { NSLog("Message found.") return notification @@ -216,6 +215,7 @@ open class FRAClientWrapper { NSLog("Message not processed yet.") pushNotification = FRAPushHandler.shared.application(application, didReceiveRemoteNotification: userInfo) if(pushNotification != nil){ + // present notification NSLog("PushNotification successfuly created: \(String(describing: pushNotification?.toJson()))") self.updatePendingNotificationsCount() return pushNotification diff --git a/forgerock-authenticator/pubspec.yaml b/forgerock-authenticator/pubspec.yaml index cf50af3..c57d733 100644 --- a/forgerock-authenticator/pubspec.yaml +++ b/forgerock-authenticator/pubspec.yaml @@ -7,7 +7,7 @@ issue_tracker: https://github.com/ForgeRock/forgerock-flutter-plugins/issues documentation: https://sdks.forgerock.com environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=3.9.0 <4.0.0' flutter: ">=1.20.0" dependencies: