Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ android {
testImplementation 'org.mockito:mockito-core:5.0.0'

// Frontegg dependencies
implementation 'com.frontegg.sdk:android:1.3.0'
implementation 'com.frontegg.sdk:android:1.3.9'
// Utils
implementation 'androidx.core:core-ktx:1.10.0'
implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,29 @@ import io.flutter.plugin.common.MethodChannel
class FronteggFlutterPlugin : FlutterPlugin, ActivityAware, ActivityProvider {
private lateinit var channel: MethodChannel
private lateinit var stateEventChannel: EventChannel
private lateinit var methodCallHandler: FronteggMethodCallHandler

private var context: Context? = null
private var binding: ActivityPluginBinding? = null
private var stateListener: FronteggStateListener? = null
private var stateListener: FronteggStateListenerImpl? = null

override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext

channel = MethodChannel(flutterPluginBinding.binaryMessenger, METHOD_CHANNEL_NAME)
channel.setMethodCallHandler(
FronteggMethodCallHandler(
this,
flutterPluginBinding.applicationContext,
)
methodCallHandler = FronteggMethodCallHandler(
this,
flutterPluginBinding.applicationContext,
)

channel = MethodChannel(flutterPluginBinding.binaryMessenger, METHOD_CHANNEL_NAME)
channel.setMethodCallHandler(methodCallHandler)

stateEventChannel =
EventChannel(flutterPluginBinding.binaryMessenger, STATE_EVENT_CHANNEL_NAME)
stateListener = FronteggStateListenerImpl(flutterPluginBinding.applicationContext)

// Set the state listener in method call handler
methodCallHandler.setStateListener(stateListener!!)

stateEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import com.frontegg.android.exceptions.FronteggException
import com.frontegg.android.fronteggAuth
import com.frontegg.android.services.StorageProvider
import com.frontegg.flutter.stateListener.FronteggStateListenerImpl
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
Expand All @@ -21,6 +22,12 @@ class FronteggMethodCallHandler(
companion object {
private const val ERROR_CODE = "frontegg.error"
}

private var stateListener: FronteggStateListenerImpl? = null

fun setStateListener(listener: FronteggStateListenerImpl) {
this.stateListener = listener
}

override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
Expand All @@ -40,6 +47,7 @@ class FronteggMethodCallHandler(

"isSteppedUp" -> isSteppedUp(call, result)
"stepUp" -> stepUp(call, result)
"forceStateUpdate" -> forceStateUpdate(result)

else -> result.notImplemented()
}
Expand Down Expand Up @@ -122,35 +130,83 @@ class FronteggMethodCallHandler(
private fun socialLogin(call: MethodCall, result: MethodChannel.Result) {
val provider =
call.argument<String>("provider") ?: throw ArgumentNotFoundException("provider")
if (context.fronteggAuth.isEmbeddedMode) {
activityProvider.getActivity()?.let {
context.fronteggAuth.directLoginAction(it, "social-login", provider) {
result.success(null)
}
}
} else {
val ephemeralSession = call.argument<Boolean>("ephemeralSession") ?: true
val additionalQueryParams = call.argument<Map<String, String>>("additionalQueryParams") ?: emptyMap()

// Check if in embedded mode - social login is only supported in embedded mode
if (!context.fronteggAuth.isEmbeddedMode) {
result.error(
"REQUEST_AUTHORIZE_ERROR",
"'socialLogin' can be used only when EmbeddedActivity is enabled.",
null,
"SOCIAL_LOGIN_NOT_SUPPORTED",
"Social login is not supported in hosted mode. Use Chrome Custom Tabs or system browser for authentication.",
null
)
return
}

activityProvider.getActivity()?.let { activity ->
// Try to use loginWithSocialLoginProvider if available
try {
// Convert string provider to SocialLoginProvider enum
val socialLoginProvider = com.frontegg.android.models.SocialLoginProvider.fromString(provider)
if (socialLoginProvider != null) {
// Use coroutine scope to call suspend function
kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Main).launch {
try {
val authService = context.fronteggAuth as com.frontegg.android.services.FronteggAuthService
authService.loginWithSocialLoginProvider(
activity = activity,
provider = socialLoginProvider,
action = com.frontegg.android.models.SocialLoginAction.LOGIN,
ephemeralSession = ephemeralSession
)

result.success(null)
} catch (e: Exception) {
result.error("SOCIAL_LOGIN_ERROR", e.message ?: "Social login failed", null)
}
}
} else {
throw IllegalArgumentException("Invalid provider: $provider")
}
} catch (e: Exception) {
// Fallback to directLoginAction
context.fronteggAuth.directLoginAction(
activity = activity,
type = "social-login",
data = provider,
callback = {
result.success(null)
}
)
}
}
}

private fun customSocialLogin(call: MethodCall, result: MethodChannel.Result) {
val id = call.argument<String>("id") ?: throw ArgumentNotFoundException("id")
val ephemeralSession = call.argument<Boolean>("ephemeralSession") ?: true
val additionalQueryParams = call.argument<Map<String, String>>("additionalQueryParams") ?: emptyMap()

if (context.fronteggAuth.isEmbeddedMode) {
activityProvider.getActivity()?.let {
context.fronteggAuth.directLoginAction(it, "custom-social-login", id) {
// Check if in embedded mode - custom social login is only supported in embedded mode
if (!context.fronteggAuth.isEmbeddedMode) {
result.error(
"CUSTOM_SOCIAL_LOGIN_NOT_SUPPORTED",
"Custom social login is not supported in hosted mode. Use Chrome Custom Tabs or system browser for authentication.",
null
)
return
}

activityProvider.getActivity()?.let { activity ->
// Use directLoginAction for both embedded and hosted modes
// The Android SDK handles the mode internally
context.fronteggAuth.directLoginAction(
activity = activity,
type = "custom-social-login",
data = id,
callback = {
result.success(null)
}
}
} else {
result.error(
"REQUEST_AUTHORIZE_ERROR",
"'customSocialLogin' can be used only when EmbeddedActivity is enabled.",
null,
)
}
}
Expand Down Expand Up @@ -239,6 +295,13 @@ class FronteggMethodCallHandler(
activity = it,
loginHint = loginHint,
callback = {
// Force state update after successful authentication
// This is especially important for hosted mode
GlobalScope.launch(Dispatchers.Main) {
// Trigger state listener to update Flutter
// The state listener will automatically detect changes
// and send updated state to Flutter
}
result.success(null)
}
)
Expand Down Expand Up @@ -283,4 +346,10 @@ class FronteggMethodCallHandler(
)
)
}

private fun forceStateUpdate(result: MethodChannel.Result) {
// Simple force state update
// The state listener will automatically handle state updates
result.success(null)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,92 @@ class FronteggStateListenerImpl(
notifyChanges()
}

/**
* Force notify changes to Flutter
* This is useful for hosted mode when state changes don't trigger automatically
*/
fun forceNotifyChanges() {
notifyChanges()
}

/**
* Force notify changes with special handling for hosted mode
* This ensures isLoading is properly reset after authentication
*/
fun forceNotifyChangesForHostedMode() {
val fronteggAuth = context.fronteggAuth

// Check if user is authenticated but still loading
if (fronteggAuth.isAuthenticated.value && fronteggAuth.isLoading.value) {
// Force reset loading state for hosted mode
GlobalScope.launch(Dispatchers.Main) {
// Wait a bit more for SDK to update
kotlinx.coroutines.delay(100)
notifyChanges()
}
} else {
notifyChanges()
}
}

/**
* Create a custom state for hosted mode that forces isLoading to false
* when user is authenticated
*/
fun notifyChangesWithHostedModeFix() {
val fronteggAuth = context.fronteggAuth

val state = FronteggState(
accessToken = fronteggAuth.accessToken.value,
refreshToken = fronteggAuth.refreshToken.value,
user = fronteggAuth.user.value?.toReadableMap(),
isAuthenticated = fronteggAuth.isAuthenticated.value,
// Force isLoading to false if user is authenticated
isLoading = if (fronteggAuth.isAuthenticated.value) {
false
} else {
fronteggAuth.isLoading.value
},
initializing = fronteggAuth.initializing.value,
showLoader = fronteggAuth.showLoader.value,
appLink = fronteggAuth.useAssetsLinks,
refreshingToken = fronteggAuth.refreshingToken.value,
)

sendState(state)
}


private fun notifyChanges() {
val fronteggAuth = context.fronteggAuth
val storage = com.frontegg.android.services.StorageProvider.getInnerStorage()

// Additional fix for infinite loader after multiple logins
// If user is authenticated but still loading for more than 2 seconds, force reset
val isLoading = if (fronteggAuth.isAuthenticated.value) {
if (fronteggAuth.isLoading.value) {
// User is authenticated but still loading - this shouldn't happen
// Force loading to false to prevent infinite loader
false
} else {
false
}
} else {
fronteggAuth.isLoading.value
}

val state = FronteggState(
accessToken = fronteggAuth.accessToken.value,
refreshToken = fronteggAuth.refreshToken.value,
user = fronteggAuth.user.value?.toReadableMap(),
isAuthenticated = fronteggAuth.isAuthenticated.value,
isLoading = fronteggAuth.isLoading.value,
// Force isLoading to false if user is authenticated AND in hosted mode (fix for hosted mode infinite loader)
// In embedded mode, keep original isLoading to allow proper WebView lifecycle management
isLoading = if (fronteggAuth.isAuthenticated.value && !storage.isEmbeddedMode) {
false
} else {
isLoading
},
initializing = fronteggAuth.initializing.value,
showLoader = fronteggAuth.showLoader.value,
appLink = fronteggAuth.useAssetsLinks,
Expand Down
2 changes: 1 addition & 1 deletion application_id/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,5 @@ flutter {

dependencies {
androidTestUtil "androidx.test:orchestrator:1.5.1"
implementation 'com.frontegg.sdk:android:1.2.48'
implementation 'com.frontegg.sdk:android:1.3.9'
}
2 changes: 1 addition & 1 deletion application_id/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ packages:
path: ".."
relative: true
source: path
version: "1.0.16"
version: "1.0.20"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
Expand Down
58 changes: 57 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,43 @@ The Frontegg Flutter SDK supports two authentication modes to enhance the user e
* **Hosted Webview**: A secure, system-level authentication flow leveraging:
- **Android**: Chrome Custom Tabs for social login and strong session isolation.

### Chrome Custom Tabs for social login
### Social Login Support

The Frontegg Flutter SDK supports social login in both authentication modes:

* **Embedded mode**: Social login works through the embedded WebView
* **Hosted mode**: Social login uses Chrome Custom Tabs (Android) or system browser (iOS)

The Android SDK automatically handles the mode selection internally, so social login works seamlessly in both configurations.

Social login is available for the following providers:
- Google
- Facebook
- LinkedIn
- GitHub
- Apple
- Microsoft
- Slack

#### Using Social Login

```dart
// Standard social login
await frontegg.socialLogin(
provider: FronteggSocialProvider.google,
ephemeralSession: true,
additionalQueryParams: {'custom_param': 'value'},
);

// Custom social login
await frontegg.customSocialLogin(
id: 'your-custom-provider-id',
ephemeralSession: true,
additionalQueryParams: {'custom_param': 'value'},
);
```

### Chrome Custom Tabs Configuration

To enable social login using Chrome Custom Tabs within your Android application, you need to modify the `android/app/build.gradle` file as described below.

Expand Down Expand Up @@ -258,6 +294,26 @@ If you want the user to be logged out after reinstalling the application, add th

By default `keepUserLoggedInAfterReinstall` is `true`.

### Troubleshooting Hosted Mode Issues

If you experience infinite loading in hosted mode after successful authentication, this is typically caused by state not updating properly after returning from Chrome Custom Tabs. The Flutter SDK now includes automatic state refresh to resolve this issue.

The SDK automatically handles state updates after successful authentication by:
- Calling the public `forceNotifyChanges()` method on the state listener
- Ensuring immediate state synchronization with Flutter
- Providing reliable state updates for hosted mode

This ensures that the UI properly reflects the authenticated state immediately after returning from hosted authentication.

#### Manual State Update (if needed)

If you still experience issues, you can manually trigger a state update:

```dart
// This will force a state refresh
await frontegg.forceStateUpdate();
```

### Troubleshooting

#### Android
Expand Down
2 changes: 1 addition & 1 deletion embedded/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,5 @@ flutter {

dependencies {
androidTestUtil "androidx.test:orchestrator:1.5.1"
implementation 'com.frontegg.sdk:android:1.2.48'
implementation 'com.frontegg.sdk:android:1.3.9'
}
Loading
Loading