Flutter Integration Guide for Hawcx
This guide will walk you through the process of integrating Hawcx into your Flutter application.
Prerequisites
- Flutter development environment set up
- Basic knowledge of Flutter and Android development
Integration Steps
- Add Hawcx to your Android project
In your flutter project, navigate to the android/app
directory and create a libs
folder if it doesn't exist. Place the Hawcx AAR file in this folder.
- Update build.gradle
In android/app/build.gradle
, add the following:
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.flutter_hawcx"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 26 // Hawcx require API 26
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
dependencies {
// Import the aar file from the libs directory
implementation fileTree(dir: 'libs', include: ['*.aar'])
// Add the dependencies that Hawcx requires
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
implementation 'com.squareup.retrofit2:retrofit:2.10.0'
implementation 'com.squareup.retrofit2:converter-gson:2.10.0'
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
implementation 'com.google.android.material:material:1.4.0'
implementation 'com.google.android.gms:play-services-location:21.2.0'
implementation 'androidx.fragment:fragment-ktx:1.3.6'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.core:core-ktx:1.6.0'
}
- Create a Native Module
Create a new kotlin file named HawcxModule.kt
in android\app\src\main\java\com\hawcxapp\
:
(following are sample methods, your implementations can change. Closely watch for all overrides)
package com.example.flutter_hawcx
import android.content.Context
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import android.os.Handler
import android.os.Looper
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import com.hawcx.HawcxInitializer
import com.hawcx.auth.SignIn
import com.hawcx.auth.SignUp
import com.hawcx.auth.Restore
import com.hawcx.utils.AuthErrorHandler
import com.hawcx.utils.AuthErrorHandler.SignInErrorCode
class HawcxModule(private val context: Context) : MethodChannel.MethodCallHandler {
private val apiKey = "YOUR_API_KEY"
private var signUp: SignUp? = null
private var signIn: SignIn? = null
private var restore: Restore? = null
init {
HawcxInitializer.getInstance().init(context, apiKey)
signUp = SignUp(context, apiKey)
signIn = SignIn(context, apiKey)
restore = Restore(context, apiKey)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"signUp" -> {
val username = call.argument<String>("username")
if (username != null) {
signUp(username, result)
} else {
result.error("ERROR", "Username is required", null)
}
}
"signIn" -> {
val username = call.argument<String>("username")
if (username != null) {
signIn(username, result)
} else {
result.error("ERROR", "Username is required", null)
}
}
"handleVerifyOTP" -> {
val username = call.argument<String>("username")
val otp = call.argument<String>("otp")
if (username != null && otp != null) {
handleVerifyOTP(username, otp, result)
} else {
result.error("ERROR", "Username and OTP are required", null)
}
}
"getLastUser" -> {
getLastUser(result)
}
"checkLastUser" -> {
checkLastUser(result)
}
"initiateBiometricLogin" -> {
initiateBiometricLogin(result, Runnable { })
}
else -> {
result.notImplemented()
}
}
}
// Public method to handle signUp
public fun signUp(username: String, result: MethodChannel.Result) {
signUp?.signUp(username, object : SignUp.SignUpCallback {
override fun showError(errorCode: String) {
result.error("SIGNUP_ERROR", "SignUp failed: $errorCode", null)
}
override fun showError(errorCode: AuthErrorHandler.SignUpErrorCode, errorMessage: String) {
result.error("SIGNUP_ERROR", "SignUp failed: $errorMessage", null)
}
override fun onGenerateOTPSuccess() {
result.success("OTP generated successfully")
}
override fun onSuccessfulSignup() {
result.success("Sign up successful")
}
})
}
// Public method to handle signIn
public fun signIn(username: String, result: MethodChannel.Result) {
try{
signIn?.signIn(username, object : SignIn.SignInCallback {
override fun showEmailSignInScreen() {
result.error("EMAIL_SIGN_IN_REQUIRED", "Please sign in with your email.", null)
}
override fun onSuccessfulLogin(username: String) {
result.success("Login successful")
}
override fun showError(errorCode: AuthErrorHandler.SignInErrorCode, message: String) {
result.error("SIGNIN_ERROR", "SignIn failed: $message", null)
}
override fun showError(message: String) {
result.error("SIGNIN_ERROR", "SignIn failed: $message", null)
}
override fun initiateBiometricLogin(callback: Runnable) {
initiateBiometricLogin(result, callback)
}
})
} catch (e: Exception) {
result.error("SIGNUP_ERROR", e.message, null)
}
}
public fun handleVerifyOTP(username: String, otp: String, result: MethodChannel.Result) {
signUp?.handleVerifyOTP(username, otp, object : SignUp.SignUpCallback {
override fun showError(errorCode: String) {
result.error("VERIFY_OTP_ERROR", "OTP verification failed: $errorCode", null)
}
override fun showError(errorCode: AuthErrorHandler.SignUpErrorCode, errorMessage: String) {
result.error("VERIFY_OTP_ERROR", "OTP verification failed: $errorMessage", null)
}
override fun onGenerateOTPSuccess() {
// Not used for OTP verification
}
override fun onSuccessfulSignup() {
result.success("OTP verified and signup complete")
}
})
}
public fun getLastUser(result: MethodChannel.Result) {
try {
val lastUser = signIn?.getLastUser() ?: ""
result.success(lastUser)
} catch (e: Exception) {
result.error("GET_LAST_USER_ERROR", "Failed to get last user: ${e.message}", null)
}
}
public fun checkLastUser(result: MethodChannel.Result) {
signIn?.checkLastUser(object : SignIn.SignInCallback {
override fun showEmailSignInScreen() {
result.success("SHOW_EMAIL_SIGN_IN_SCREEN")
}
override fun onSuccessfulLogin(username: String) {
result.success("Login successful for user: $username")
}
override fun showError(errorCode: AuthErrorHandler.SignInErrorCode, message: String) {
result.error("CHECK_LAST_USER_ERROR", "Check last user failed: $message", null)
}
override fun showError(message: String) {
result.error("CHECK_LAST_USER_ERROR", "Check last user failed: $message", null)
}
override fun initiateBiometricLogin(callback: Runnable) {
initiateBiometricLogin(result, callback)
}
})
}
public fun initiateBiometricLogin(result: MethodChannel.Result, callback: Runnable) {
val currentActivity = (context as? androidx.fragment.app.FragmentActivity)
if (currentActivity == null) {
result.error("ACTIVITY_ERROR", "Current activity is not available", null)
return
}
val executor = ContextCompat.getMainExecutor(context)
val biometricPrompt = BiometricPrompt(
currentActivity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errorMessage: CharSequence) {
result.error("BIOMETRIC_AUTH_ERROR", "Authentication error: $errorMessage", null)
}
override fun onAuthenticationSucceeded(authResult: BiometricPrompt.AuthenticationResult) {
callback.run()
}
override fun onAuthenticationFailed() {
result.error("BIOMETRIC_AUTH_FAILED", "Authentication failed", null)
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric Login")
.setDescription("Use your fingerprint or face to log in")
.setNegativeButtonText("Cancel")
.build()
biometricPrompt.authenticate(promptInfo)
}
public fun generateOtpForAccountRestore(email: String, result: MethodChannel.Result) {
restore?.generateOtp(email, object : Restore.OnSuccessListener {
override fun onSuccess(message: String) {
result.success(message)
}
}, object : Restore.OnErrorListener {
override fun onError(error: String) {
result.error("GENERATE_OTP_ERROR", error, null)
}
})
}
public fun verifyOtpForAccountRestore(email: String, otp: String, result: MethodChannel.Result) {
restore?.verifyOtp(email, otp, object : Restore.OnSuccessListener {
override fun onSuccess(message: String) {
result.success(message)
}
}, object : Restore.OnErrorListener {
override fun onError(error: String) {
result.error("VERIFY_OTP_ERROR", error, null)
}
})
}
// Add other methods for different HawcxAuth features
}
- Use module in MainActivity.kt
In MainActivity.kt
, implement the channel calls to communicate with dart and use FlutterFragmentActivity:
(You can change the implementation based on your need)
package com.example.flutter_hawcx
import android.os.Bundle
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.plugin.common.MethodChannel
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
import com.example.flutter_hawcx.HawcxModule
class MainActivity: FlutterFragmentActivity() {
private val CHANNEL = "com.example.flutter_hawcx/hawcx"
private lateinit var hawcxModule: HawcxModule
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize HawcxModule
hawcxModule = HawcxModule(this)
}
// Ensure flutterEngine is initialized to avoid nullability issues
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
// Set up the method channel to communicate with Flutter
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
// Sign-up functionality
"signUp" -> {
val username = call.argument<String>("username")
if (username != null) {
hawcxModule.signUp(username, result)
} else {
result.error("ERROR", "Username is required", null)
}
}
// Sign-in functionality
"signIn" -> {
val username = call.argument<String>("username")
if (username != null) {
hawcxModule.signIn(username, result)
} else {
result.error("ERROR", "Username is required", null)
}
}
// OTP Verification functionality
"handleVerifyOTP" -> {
val username = call.argument<String>("username")
val otp = call.argument<String>("otp")
if (username != null && otp != null) {
hawcxModule.handleVerifyOTP(username, otp, result)
} else {
result.error("ERROR", "Username and OTP are required", null)
}
}
// Get last signed-in user
"getLastUser" -> {
hawcxModule.getLastUser(result)
}
// Check last signed-in user
"checkLastUser" -> {
hawcxModule.checkLastUser(result)
}
// Initiate biometric login
"initiateBiometricLogin" -> {
hawcxModule.initiateBiometricLogin(result, Runnable { })
}
// Generate OTP for account restore
"generateOtpForAccountRestore" -> {
val email = call.argument<String>("email")
if (email != null) {
hawcxModule.generateOtpForAccountRestore(email, result)
} else {
result.error("ERROR", "Email is required", null)
}
}
// Verify OTP for account restore
"verifyOtpForAccountRestore" -> {
val email = call.argument<String>("email")
val otp = call.argument<String>("otp")
if (email != null && otp != null) {
hawcxModule.verifyOtpForAccountRestore(email, otp, result)
} else {
result.error("ERROR", "Email and OTP are required", null)
}
}
// Default handler for unknown method calls
else -> {
result.notImplemented()
}
}
}
}
}
android\app\src\main\AndroidManifest.xml
You need to add biometric access to the to the AndroidManifest.xml
as following:
(you can copy the following xml config or check your config for xmlns:tools, permission, feature, android:supportsRt and tools:replace)
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<!-- Permissions required for biometric authentication -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- Features related to biometrics -->
<uses-feature
android:name="android.hardware.fingerprint"
android:required="false" />
<uses-feature
android:name="android.hardware.biometrics"
android:required="false" />
<application
android:label="flutter_hawcx"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true"
tools:replace="android:label">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
Add local_auth in pubspec.yaml
Now you can use HawcxAuth in your Flutter widgets:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // For Native MethodChannel calls
import 'package:email_validator/email_validator.dart'; // Optional email validation package
class SignupScreen extends StatefulWidget {
@override
_SignupScreenState createState() => _SignupScreenState();
}
class _SignupScreenState extends State<SignupScreen> {
static const platform = MethodChannel(
'com.example.flutter_hawcx/hawcx'); // Similar to NativeModules in React Native
final TextEditingController _emailController = TextEditingController();
bool _loading = false; // Loading state to manage the signup process
@override
void initState() {
super.initState();
checkLastUser(); // Similar to useEffect in React Native
}
// Email validation
bool validateEmail(String email) {
return EmailValidator.validate(email);
}
// Check if a user exists and handle biometric login
Future<void> checkLastUser() async {
setState(() {
_loading = true; // Show loading indicator
});
try {
final String result = await platform.invokeMethod('checkLastUser');
print(result);
if (result.contains("Login successful for user")) {
setState(() {
_loading = false; // Hide loading indicator
});
Navigator.pushReplacementNamed(context, '/home');
} else if (result == 'SHOW_EMAIL_SIGN_IN_SCREEN') {
setState(() {
_loading = false; // Hide loading indicator
});
Navigator.pushReplacementNamed(context, '/signin');
}
} on PlatformException catch (e) {
print("Error checking last user: ${e.message}");
setState(() {
_loading = false; // Hide loading indicator
});
}
}
// Handle signup process
void handleSignup() async {
String email = _emailController.text.trim();
if (email.isNotEmpty && validateEmail(email)) {
setState(() {
_loading = true; // Show loading state
});
try {
await platform.invokeMethod('signUp', {'username': email});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('OTP sent for verification!')));
Navigator.pushNamed(context, '/verification', arguments: email);
} on PlatformException catch (e) {
// Check if the error message contains 'User already exists'
if (e.message != null && e.message!.contains('User already exists')) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('User already exists. Please sign in.')));
Navigator.pushNamed(context, '/signin');
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Sign up failed: ${e.message}')));
}
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Sign up failed')));
} finally {
setState(() {
_loading = false; // Hide loading state
});
}
} else if (email.isEmpty) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Please enter your email')));
} else {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Please enter a valid email')));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Sign Up')),
body: Stack(
children: [
Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Enter your Email to Sign Up:',
style: TextStyle(fontSize: 18),
),
SizedBox(height: 12),
TextField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 12),
ElevatedButton(
onPressed: _loading ? null : handleSignup,
child: Text(_loading ? 'Signing Up...' : 'Sign Up'),
),
SizedBox(height: 12),
TextButton(
onPressed: () => Navigator.pushNamed(context, '/signin'),
child: Text('Already have an account? Sign In'),
),
],
),
),
// Show a circular progress indicator when _loading is true
if (_loading)
Center(
child: CircularProgressIndicator(),
),
],
),
);
}
}
Best Practices
- Always handle errors and exceptions from native methods.
- Use async/await for cleaner code when calling native methods.
- Implement proper error handling and user feedback in your React Native UI.