From b023c374f47508c9cb3cc24259914dd3f89b0143 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Sat, 7 Oct 2023 17:29:53 +0300 Subject: [PATCH] add embedded mode authentication activity --- .gitignore | 1 + README.md | 1 - android/build.gradle | 1 + android/src/main/AndroidManifest.xml | 6 + .../android/AuthenticationActivity.kt | 19 +- .../java/com/frontegg/android/FronteggApp.kt | 6 +- .../java/com/frontegg/android/FronteggAuth.kt | 20 +- .../android/embedded/EmbeddedAuthActivity.kt | 97 ++++++++ .../android/embedded/FronteggWebClient.kt | 233 ++++++++++++++++++ .../android/embedded/FronteggWebView.kt | 37 +++ .../com/frontegg/android/utils/Constants.kt | 22 ++ .../res/layout/activity_embedded_auth.xml | 28 +++ .../res/layout/activity_frontegg_login.xml | 2 +- app/build.gradle | 1 + .../java/com/frontegg/demo/AuthFragment.kt | 5 +- .../demo/ui/tenants/TenantsFragment.kt | 2 +- build.gradle | 2 +- embedded/.gitignore | 1 + embedded/build.gradle | 86 +++++++ embedded/proguard-rules.pro | 21 ++ .../frontegg/demo/ExampleInstrumentedTest.kt | 24 ++ embedded/src/main/AndroidManifest.xml | 30 +++ .../src/main/java/com/frontegg/demo/App.kt | 22 ++ .../java/com/frontegg/demo/AuthFragment.kt | 51 ++++ .../com/frontegg/demo/NavigationActivity.kt | 118 +++++++++ .../com/frontegg/demo/ui/home/HomeFragment.kt | 59 +++++ .../frontegg/demo/ui/home/HomeViewModel.kt | 25 ++ .../demo/ui/tenants/TenantsFragment.kt | 112 +++++++++ .../demo/ui/tenants/TenantsViewModel.kt | 32 +++ .../drawable-v24/ic_launcher_foreground.xml | 30 +++ .../main/res/drawable/baseline_person_24.xml | 10 + .../res/drawable/ic_dashboard_black_24dp.xml | 9 + .../main/res/drawable/ic_home_black_24dp.xml | 9 + .../res/drawable/ic_launcher_background.xml | 170 +++++++++++++ .../drawable/ic_notifications_black_24dp.xml | 9 + .../main/res/layout/activity_navigation.xml | 33 +++ .../src/main/res/layout/fragment_auth.xml | 30 +++ .../src/main/res/layout/fragment_home.xml | 78 ++++++ .../src/main/res/layout/fragment_tenants.xml | 16 ++ embedded/src/main/res/layout/loader.xml | 24 ++ .../src/main/res/layout/tenant_row_item.xml | 26 ++ .../src/main/res/menu/bottom_nav_menu.xml | 14 ++ embedded/src/main/res/menu/menu_main.xml | 10 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../src/main/res/navigation/navigation.xml | 19 ++ .../res/navigation/not_auth_navigation.xml | 12 + embedded/src/main/res/values/colors.xml | 10 + embedded/src/main/res/values/dimens.xml | 6 + embedded/src/main/res/values/strings.xml | 16 ++ embedded/src/main/res/values/themes.xml | 25 ++ .../java/com/frontegg/demo/ExampleUnitTest.kt | 17 ++ settings.gradle | 1 + 63 files changed, 1627 insertions(+), 23 deletions(-) create mode 100644 android/src/main/java/com/frontegg/android/embedded/EmbeddedAuthActivity.kt create mode 100644 android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt create mode 100644 android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt create mode 100644 android/src/main/res/layout/activity_embedded_auth.xml create mode 100644 embedded/.gitignore create mode 100644 embedded/build.gradle create mode 100644 embedded/proguard-rules.pro create mode 100644 embedded/src/androidTest/java/com/frontegg/demo/ExampleInstrumentedTest.kt create mode 100644 embedded/src/main/AndroidManifest.xml create mode 100644 embedded/src/main/java/com/frontegg/demo/App.kt create mode 100644 embedded/src/main/java/com/frontegg/demo/AuthFragment.kt create mode 100644 embedded/src/main/java/com/frontegg/demo/NavigationActivity.kt create mode 100644 embedded/src/main/java/com/frontegg/demo/ui/home/HomeFragment.kt create mode 100644 embedded/src/main/java/com/frontegg/demo/ui/home/HomeViewModel.kt create mode 100644 embedded/src/main/java/com/frontegg/demo/ui/tenants/TenantsFragment.kt create mode 100644 embedded/src/main/java/com/frontegg/demo/ui/tenants/TenantsViewModel.kt create mode 100644 embedded/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 embedded/src/main/res/drawable/baseline_person_24.xml create mode 100644 embedded/src/main/res/drawable/ic_dashboard_black_24dp.xml create mode 100644 embedded/src/main/res/drawable/ic_home_black_24dp.xml create mode 100644 embedded/src/main/res/drawable/ic_launcher_background.xml create mode 100644 embedded/src/main/res/drawable/ic_notifications_black_24dp.xml create mode 100644 embedded/src/main/res/layout/activity_navigation.xml create mode 100644 embedded/src/main/res/layout/fragment_auth.xml create mode 100644 embedded/src/main/res/layout/fragment_home.xml create mode 100644 embedded/src/main/res/layout/fragment_tenants.xml create mode 100644 embedded/src/main/res/layout/loader.xml create mode 100644 embedded/src/main/res/layout/tenant_row_item.xml create mode 100644 embedded/src/main/res/menu/bottom_nav_menu.xml create mode 100644 embedded/src/main/res/menu/menu_main.xml create mode 100644 embedded/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 embedded/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 embedded/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 embedded/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 embedded/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 embedded/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 embedded/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 embedded/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 embedded/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 embedded/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 embedded/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 embedded/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 embedded/src/main/res/navigation/navigation.xml create mode 100644 embedded/src/main/res/navigation/not_auth_navigation.xml create mode 100644 embedded/src/main/res/values/colors.xml create mode 100644 embedded/src/main/res/values/dimens.xml create mode 100644 embedded/src/main/res/values/strings.xml create mode 100644 embedded/src/main/res/values/themes.xml create mode 100644 embedded/src/test/java/com/frontegg/demo/ExampleUnitTest.kt diff --git a/.gitignore b/.gitignore index aa724b7..8ea58eb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ .externalNativeBuild .cxx local.properties +embedded/build diff --git a/README.md b/README.md index cb0c26f..f0b0a9a 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,6 @@ android { Add `INTERNET` permission to the app's manifest file. ```xml - ``` diff --git a/android/build.gradle b/android/build.gradle index 7512564..1dbc512 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -55,6 +55,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" implementation "androidx.browser:browser:1.6.0" implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.5.0' + implementation 'androidx.core:core-ktx:+' } afterEvaluate { diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 4a0f89a..767ca38 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -5,6 +5,11 @@ + + + @@ -50,6 +55,7 @@ + diff --git a/android/src/main/java/com/frontegg/android/AuthenticationActivity.kt b/android/src/main/java/com/frontegg/android/AuthenticationActivity.kt index ce8a424..9b459ac 100644 --- a/android/src/main/java/com/frontegg/android/AuthenticationActivity.kt +++ b/android/src/main/java/com/frontegg/android/AuthenticationActivity.kt @@ -8,6 +8,7 @@ import android.net.Uri import android.os.Bundle import android.util.Log import androidx.browser.customtabs.CustomTabsIntent +import com.frontegg.android.embedded.EmbeddedAuthActivity import com.frontegg.android.utils.AuthorizeUrlGenerator class AuthenticationActivity : Activity() { @@ -49,7 +50,7 @@ class AuthenticationActivity : Activity() { } } else { val intentUrl = intent.data - if(intentUrl == null){ + if (intentUrl == null) { setResult(RESULT_CANCELED) finish() return @@ -116,12 +117,16 @@ class AuthenticationActivity : Activity() { private val TAG = AuthenticationActivity::class.java.simpleName fun authenticateUsingBrowser(activity: Activity) { - val intent = Intent(activity, AuthenticationActivity::class.java) - val authorizeUri = AuthorizeUrlGenerator().generate() - intent.putExtra(AUTH_LAUNCHED, true) - intent.putExtra(AUTHORIZE_URI, authorizeUri.first) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - activity.startActivityForResult(intent, OAUTH_LOGIN_REQUEST) + if (FronteggApp.getInstance().isEmbeddedMode) { + EmbeddedAuthActivity.authenticate(activity) + } else { + val intent = Intent(activity, AuthenticationActivity::class.java) + val authorizeUri = AuthorizeUrlGenerator().generate() + intent.putExtra(AUTH_LAUNCHED, true) + intent.putExtra(AUTHORIZE_URI, authorizeUri.first) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + activity.startActivityForResult(intent, OAUTH_LOGIN_REQUEST) + } } } } diff --git a/android/src/main/java/com/frontegg/android/FronteggApp.kt b/android/src/main/java/com/frontegg/android/FronteggApp.kt index 8696f1a..47ae585 100644 --- a/android/src/main/java/com/frontegg/android/FronteggApp.kt +++ b/android/src/main/java/com/frontegg/android/FronteggApp.kt @@ -10,7 +10,8 @@ import com.frontegg.android.services.* class FronteggApp private constructor( val context: Context, val baseUrl: String, - val clientId: String + val clientId: String, + val isEmbeddedMode:Boolean ) { val credentialManager: CredentialManager = CredentialManager(context) @@ -33,6 +34,7 @@ class FronteggApp private constructor( fronteggDomain: String, clientId: String, context: Context, + isEmbeddedMode:Boolean = false, ) { val baseUrl: String = if (fronteggDomain.startsWith("https")) { throw FronteggException(FRONTEGG_DOMAIN_MUST_NOT_START_WITH_HTTPS) @@ -40,7 +42,7 @@ class FronteggApp private constructor( "https://$fronteggDomain" } - instance = FronteggApp(context, baseUrl, clientId) + instance = FronteggApp(context, baseUrl, clientId, isEmbeddedMode) } } diff --git a/android/src/main/java/com/frontegg/android/FronteggAuth.kt b/android/src/main/java/com/frontegg/android/FronteggAuth.kt index b104fcc..4bc213e 100644 --- a/android/src/main/java/com/frontegg/android/FronteggAuth.kt +++ b/android/src/main/java/com/frontegg/android/FronteggAuth.kt @@ -6,6 +6,7 @@ import android.os.Handler import android.os.Looper import android.util.Log import android.webkit.CookieManager +import com.frontegg.android.embedded.EmbeddedAuthActivity import com.frontegg.android.models.User import com.frontegg.android.services.Api import com.frontegg.android.services.CredentialManager @@ -18,6 +19,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import java.lang.Exception import java.time.Instant import java.util.* import kotlin.concurrent.schedule @@ -88,12 +90,18 @@ class FronteggAuth( private fun refreshTokenIfNeeded(): Boolean { val refreshToken = this.refreshToken.value ?: return false Log.d(TAG, "refreshTokenIfNeeded()") - val data = api.refreshToken(refreshToken) - return if (data != null) { - setCredentials(data.access_token, data.refresh_token) - true - } else { - false + + return try { + val data = api.refreshToken(refreshToken) + if (data != null) { + setCredentials(data.access_token, data.refresh_token) + true + } else { + false + } + } catch (e: Exception) { + Log.e(TAG, "Failed to send refresh token request", e) + false; } } diff --git a/android/src/main/java/com/frontegg/android/embedded/EmbeddedAuthActivity.kt b/android/src/main/java/com/frontegg/android/embedded/EmbeddedAuthActivity.kt new file mode 100644 index 0000000..0eb870a --- /dev/null +++ b/android/src/main/java/com/frontegg/android/embedded/EmbeddedAuthActivity.kt @@ -0,0 +1,97 @@ +package com.frontegg.android.embedded + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.LinearLayout +import com.frontegg.android.FronteggAuth +import com.frontegg.android.R +import com.frontegg.android.utils.AuthorizeUrlGenerator +import com.frontegg.android.utils.NullableObject +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.functions.Consumer + +class EmbeddedAuthActivity : Activity() { + + lateinit var webView: FronteggWebView + var webViewUrl: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_embedded_auth) + overridePendingTransition(R.anim.fadein, R.anim.fadeout) + + webView = findViewById(R.id.custom_webview) + loaderLayout = findViewById(R.id.loaderView) + + val authorizeUrl = AuthorizeUrlGenerator() + val url = authorizeUrl.generate() + + webViewUrl = url.first + + + if (!FronteggAuth.instance.initializing.value + && !FronteggAuth.instance.isAuthenticated.value + ) { + webView.loadUrl(webViewUrl!!) + webViewUrl = null + } + + } + + private val disposables: ArrayList = arrayListOf() + private var loaderLayout: LinearLayout? = null + + private val showLoaderConsumer: Consumer> = Consumer { + Log.d(TAG, "showLoaderConsumer: ${it.value}") + runOnUiThread { + loaderLayout?.visibility = if (it.value) View.VISIBLE else View.GONE + } + } + private val isAuthenticatedConsumer: Consumer> = Consumer { + Log.d(TAG, "isAuthenticatedConsumer: ${it.value}") + if (it.value) { + runOnUiThread { + navigateToAuthenticated() + } + } + } + + private fun navigateToAuthenticated() { + setResult(RESULT_OK) + finish() + } + override fun onResume() { + super.onResume() + disposables.add(FronteggAuth.instance.showLoader.subscribe(this.showLoaderConsumer)) + disposables.add(FronteggAuth.instance.isAuthenticated.subscribe(this.isAuthenticatedConsumer)) + + } + + override fun onPause() { + super.onPause() + disposables.forEach { + it.dispose() + } + } + + + companion object { + const val OAUTH_LOGIN_REQUEST = 100001 + const val AUTHORIZE_URI = "com.frontegg.android.AUTHORIZE_URI" + private const val AUTH_LAUNCHED = "com.frontegg.android.AUTH_LAUNCHED" + private val TAG = EmbeddedAuthActivity::class.java.simpleName + + fun authenticate(activity: Activity) { + val intent = Intent(activity, EmbeddedAuthActivity::class.java) + + val authorizeUri = AuthorizeUrlGenerator().generate() + intent.putExtra(AUTH_LAUNCHED, true) +// intent.putExtra(AUTHORIZE_URI, authorizeUri.first) +// intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + activity.startActivityForResult(intent, OAUTH_LOGIN_REQUEST) + } + } +} diff --git a/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt b/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt new file mode 100644 index 0000000..9cf334b --- /dev/null +++ b/android/src/main/java/com/frontegg/android/embedded/FronteggWebClient.kt @@ -0,0 +1,233 @@ +package com.frontegg.android.embedded + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.net.UrlQuerySanitizer +import android.util.Log +import android.webkit.* +import com.frontegg.android.FronteggApp +import com.frontegg.android.FronteggAuth +import com.frontegg.android.utils.AuthorizeUrlGenerator +import com.frontegg.android.utils.Constants +import com.frontegg.android.utils.Constants.Companion.loginRoutes +import com.frontegg.android.utils.Constants.Companion.oauthUrls +import com.frontegg.android.utils.Constants.Companion.socialLoginRedirectUrl +import com.frontegg.android.utils.Constants.Companion.successLoginRoutes + + +class FronteggWebClient(val context: Context) : WebViewClient() { + companion object { + private val TAG = FronteggWebClient::class.java.simpleName + } + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + Log.d(TAG, "onPageStarted $url") + FronteggAuth.instance.isLoading.value = true + + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + Log.d(TAG, "onPageFinished $url") + when (getOverrideUrlType(Uri.parse(url))) { + OverrideUrlType.HostedLoginCallback -> + FronteggAuth.instance.isLoading.value = true + + OverrideUrlType.Unknown, + OverrideUrlType.loginRoutes -> + FronteggAuth.instance.isLoading.value = false + + else -> + FronteggAuth.instance.isLoading.value = true + } + } + + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError? + ) { + super.onReceivedError(view, request, error) + } + + override fun onReceivedHttpError( + view: WebView?, + request: WebResourceRequest?, + errorResponse: WebResourceResponse? + ) { + if (view!!.url == request?.url.toString()) { + Log.d(TAG, "onReceivedHttpError: Direct load url, ${request?.url?.path}") + } else { + Log.d(TAG, "onReceivedHttpError: HTTP api call, ${request?.url?.path}") + } + super.onReceivedHttpError(view, request, errorResponse) + } + + private fun setUriParameter(uri: Uri, key: String, newValue: String): Uri? { + val params: Set = uri.queryParameterNames + val newUri: Uri.Builder = uri.buildUpon().clearQuery() + var paramExist = false + for (param in params) { + if (param == key) { + paramExist = true + newUri.appendQueryParameter(param, newValue) + } else { + newUri.appendQueryParameter(param, uri.getQueryParameter(param)) + } + } + if (!paramExist) { + newUri.appendQueryParameter(key, newValue) + } + return newUri.build() + } + + enum class OverrideUrlType { + HostedLoginCallback, + SocialLoginRedirectToBrowser, + SocialOauthPreLogin, + loginRoutes, + internalRoutes, + Unknown + } + + private fun getOverrideUrlType(url: Uri): OverrideUrlType { + val urlPath = url.path + val hostedLoginCallback = Constants.oauthCallbackUrl(FronteggApp.getInstance().baseUrl); + + if (url.toString().startsWith(hostedLoginCallback)) { + return OverrideUrlType.HostedLoginCallback + } + if (urlPath != null && url.toString().startsWith(FronteggApp.getInstance().baseUrl)) { + + if (urlPath.startsWith("/frontegg/identity/resources/auth/v2/user/sso/default") + && urlPath.endsWith("/prelogin") + ) { + return OverrideUrlType.SocialOauthPreLogin + } + + return if (successLoginRoutes.find { u -> urlPath.startsWith(u) } != null) { + OverrideUrlType.internalRoutes + } else if (loginRoutes.find { u -> urlPath.startsWith(u) } != null) { + OverrideUrlType.loginRoutes + } else { + OverrideUrlType.internalRoutes + } + } + + if (oauthUrls.find { u -> url.toString().startsWith(u) } != null) { + return OverrideUrlType.SocialLoginRedirectToBrowser + } + + + return OverrideUrlType.Unknown + } + + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + val url = request?.url + + if (url != null) { + when (getOverrideUrlType(request.url)) { + OverrideUrlType.HostedLoginCallback -> { + return handleHostedLoginCallback(view, request.url?.query) + } + + OverrideUrlType.SocialLoginRedirectToBrowser -> { + FronteggAuth.instance.isLoading.value = true + val browserIntent = Intent(Intent.ACTION_VIEW, url) + context.startActivity(browserIntent) + return true + } + + else -> { + return super.shouldOverrideUrlLoading(view, request) + } + } + } else { + return super.shouldOverrideUrlLoading(view, request) + } + + } + + + override fun onLoadResource(view: WebView?, url: String?) { + if (url == null || view == null) { + super.onLoadResource(view, url) + return + } + + val uri = Uri.parse(url) + val urlType = getOverrideUrlType(uri) + val query = uri.query + when (urlType) { + OverrideUrlType.HostedLoginCallback -> { + if (handleHostedLoginCallback(view, query)) { + return + } + super.onLoadResource(view, url) + } + + OverrideUrlType.SocialOauthPreLogin -> { + if (setSocialLoginRedirectUri(view, uri)) { + return + } + super.onLoadResource(view, url) + } + + else -> { + super.onLoadResource(view, url) + } + } + } + + private fun setSocialLoginRedirectUri(webView: WebView, uri: Uri): Boolean { + Log.d(TAG, "setSocialLoginRedirectUri setting redirect uri for social login") + + if (uri.getQueryParameter("redirectUri") != null) { + Log.d(TAG, "redirectUri exist, forward navigation to webView") + return false + } + val baseUrl = FronteggApp.getInstance().baseUrl + val oauthRedirectUri = socialLoginRedirectUrl(baseUrl) + val newUri = setUriParameter(uri, "redirectUri", oauthRedirectUri) + webView.loadUrl(newUri.toString()) + return true + } + + + private fun handleHostedLoginCallback(webView: WebView?, query: String?): Boolean { + Log.d(TAG, "handleHostedLoginCallback received query: $query") + if (query == null || webView == null) { + Log.d( + TAG, + "handleHostedLoginCallback failed of nullable value, query: $query, webView: $webView" + ) + return false + } + + val querySanitizer = UrlQuerySanitizer() + querySanitizer.allowUnregisteredParamaters = true + querySanitizer.parseQuery(query) + val code = querySanitizer.getValue("code") + + if (code == null) { + Log.d(TAG, "handleHostedLoginCallback failed of nullable code value, query: $query") + return false + } + + if(FronteggAuth.instance.handleHostedLoginCallback(code)){ + return true; + } + + val authorizeUrl = AuthorizeUrlGenerator() + val url = authorizeUrl.generate() + webView.loadUrl(url.first) + return false + } + +} \ No newline at end of file diff --git a/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt b/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt new file mode 100644 index 0000000..a1c927c --- /dev/null +++ b/android/src/main/java/com/frontegg/android/embedded/FronteggWebView.kt @@ -0,0 +1,37 @@ +package com.frontegg.android.embedded + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.webkit.WebView +import com.frontegg.android.utils.AuthorizeUrlGenerator +import java.util.* + + +open class FronteggWebView : WebView { + + constructor(context: Context) : super(context) { + initView(context) + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + initView(context) + } + + @SuppressLint("SetJavaScriptEnabled") + fun initView(context: Context) { + settings.javaScriptEnabled = true + settings.useWideViewPort = true + settings.loadWithOverviewMode = true + settings.domStorageEnabled = true + + webViewClient = FronteggWebClient(context) + } + + fun loadOauthAuthorize() { + val authorizeUrl = AuthorizeUrlGenerator() + val url = authorizeUrl.generate() + this.loadUrl(url.first) + } +} + diff --git a/android/src/main/java/com/frontegg/android/utils/Constants.kt b/android/src/main/java/com/frontegg/android/utils/Constants.kt index 3c79a47..371693e 100644 --- a/android/src/main/java/com/frontegg/android/utils/Constants.kt +++ b/android/src/main/java/com/frontegg/android/utils/Constants.kt @@ -11,12 +11,30 @@ class ApiConstants { const val exchangeToken: String = "oauth/token" const val logout: String = "identity/resources/auth/v1/logout" const val switchTenant: String = "identity/resources/users/v1/tenant" + + } } class Constants { companion object { + val oauthUrls = listOf( + "https://www.facebook.com", + "https://accounts.google.com", + "https://github.com/login/oauth/authorize", + "https://login.microsoftonline.com", + "https://slack.com/openid/connect/authorize", + "https://appleid.apple.com", + "https://www.linkedin.com/oauth/" + ) + + val successLoginRoutes = listOf( + "/oauth/account/social/success", + ) + val loginRoutes = listOf( + "/oauth/account/", + ) fun oauthCallbackUrl(baseUrl: String): String { @@ -26,5 +44,9 @@ class Constants { return "${packageName}://${host}/android/oauth/callback" } + fun socialLoginRedirectUrl(baseUrl: String): String { + return "$baseUrl/oauth/account/social/success" + } + } } \ No newline at end of file diff --git a/android/src/main/res/layout/activity_embedded_auth.xml b/android/src/main/res/layout/activity_embedded_auth.xml new file mode 100644 index 0000000..b8699a9 --- /dev/null +++ b/android/src/main/res/layout/activity_embedded_auth.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/android/src/main/res/layout/activity_frontegg_login.xml b/android/src/main/res/layout/activity_frontegg_login.xml index f8de77b..6e36fb7 100644 --- a/android/src/main/res/layout/activity_frontegg_login.xml +++ b/android/src/main/res/layout/activity_frontegg_login.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + diff --git a/app/build.gradle b/app/build.gradle index 9242747..bfd0f33 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,6 +65,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation 'de.hdodenhof:circleimageview:3.1.0' implementation 'com.github.bumptech.glide:glide:4.12.0' + implementation 'androidx.core:core-ktx:+' androidTestImplementation 'org.jetbrains.kotlin:kotlin-stdlib:' + rootProject.kotlinVersion androidTestImplementation 'androidx.test:core:' + rootProject.coreVersion diff --git a/app/src/main/java/com/frontegg/demo/AuthFragment.kt b/app/src/main/java/com/frontegg/demo/AuthFragment.kt index f5328c3..0d42bac 100644 --- a/app/src/main/java/com/frontegg/demo/AuthFragment.kt +++ b/app/src/main/java/com/frontegg/demo/AuthFragment.kt @@ -1,12 +1,10 @@ package com.frontegg.demo -import android.app.Activity import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment import com.frontegg.android.FronteggAuth import com.frontegg.demo.databinding.FragmentAuthBinding import io.reactivex.rxjava3.disposables.Disposable @@ -37,7 +35,6 @@ class AuthFragment : Fragment() { super.onViewCreated(view, savedInstanceState) binding.loginButton.setOnClickListener { - FronteggAuth.instance.login(requireActivity()) } } diff --git a/app/src/main/java/com/frontegg/demo/ui/tenants/TenantsFragment.kt b/app/src/main/java/com/frontegg/demo/ui/tenants/TenantsFragment.kt index 98a433f..cc591ee 100644 --- a/app/src/main/java/com/frontegg/demo/ui/tenants/TenantsFragment.kt +++ b/app/src/main/java/com/frontegg/demo/ui/tenants/TenantsFragment.kt @@ -54,7 +54,7 @@ class CustomAdapter(context: Context, private val dataSource: MutableList + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/embedded/src/main/java/com/frontegg/demo/App.kt b/embedded/src/main/java/com/frontegg/demo/App.kt new file mode 100644 index 0000000..2b1851a --- /dev/null +++ b/embedded/src/main/java/com/frontegg/demo/App.kt @@ -0,0 +1,22 @@ +package com.frontegg.demo + +import android.app.Application +import com.frontegg.android.FronteggApp + +class App : Application() { + + companion object { + public lateinit var instance: App + } + override fun onCreate() { + super.onCreate() + instance = this + FronteggApp.init( + BuildConfig.FRONTEGG_DOMAIN, + BuildConfig.FRONTEGG_CLIENT_ID, + this, + BuildConfig.FRONTEGG_EMBEDDED_MODE, + + ) + } +} \ No newline at end of file diff --git a/embedded/src/main/java/com/frontegg/demo/AuthFragment.kt b/embedded/src/main/java/com/frontegg/demo/AuthFragment.kt new file mode 100644 index 0000000..416a6a1 --- /dev/null +++ b/embedded/src/main/java/com/frontegg/demo/AuthFragment.kt @@ -0,0 +1,51 @@ +package com.frontegg.demo + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.frontegg.android.FronteggAuth +import com.frontegg.demo.databinding.FragmentAuthBinding +import io.reactivex.rxjava3.disposables.Disposable + +/** + * A simple [Fragment] subclass as the default destination in the navigation. + */ +class AuthFragment : Fragment() { + + private var _binding: FragmentAuthBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + private val disposables: ArrayList = arrayListOf() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + + _binding = FragmentAuthBinding.inflate(inflater, container, false) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.loginButton.setOnClickListener { + + FronteggAuth.instance.login(requireActivity()) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + disposables.forEach { + it.dispose() + } + disposables.clear() + } +} \ No newline at end of file diff --git a/embedded/src/main/java/com/frontegg/demo/NavigationActivity.kt b/embedded/src/main/java/com/frontegg/demo/NavigationActivity.kt new file mode 100644 index 0000000..d8e0f0b --- /dev/null +++ b/embedded/src/main/java/com/frontegg/demo/NavigationActivity.kt @@ -0,0 +1,118 @@ +package com.frontegg.demo + +import android.os.Bundle +import android.util.Log +import android.view.View +import com.google.android.material.bottomnavigation.BottomNavigationView +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.NavController +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.setupActionBarWithNavController +import androidx.navigation.ui.setupWithNavController +import com.frontegg.android.FronteggAuth +import com.frontegg.android.utils.NullableObject +import com.frontegg.demo.databinding.ActivityNavigationBinding +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.functions.Consumer + +class NavigationActivity : AppCompatActivity() { + + companion object { + private val TAG = NavigationActivity::class.java.canonicalName + } + + private lateinit var binding: ActivityNavigationBinding + private var fronteggAuth = FronteggAuth.instance + private lateinit var navController: NavController + + + private val authenticatedTabs = AppBarConfiguration( + setOf( + R.id.navigation_home, R.id.navigation_tenants + ) + ) + private val nonAuthTabs = AppBarConfiguration( + setOf( + R.id.navigation_login + ) + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityNavigationBinding.inflate(layoutInflater) + setContentView(binding.root) + + val navView: BottomNavigationView = binding.navView + + navController = findNavController(R.id.nav_host_fragment_activity_navigation) + + navView.setupWithNavController(navController) + setupActionBarWithNavController(navController, nonAuthTabs) + + navController.setGraph(R.navigation.not_auth_navigation) + + } + private val disposables: ArrayList = arrayListOf() + override fun onResume() { + super.onResume() + disposables.add(FronteggAuth.instance.showLoader.subscribe(this.onShowLoaderChange)) + disposables.add(FronteggAuth.instance.isAuthenticated.subscribe(this.onIsAuthenticatedChange)) + + } + + override fun onPause() { + super.onPause() + disposables.forEach { + it.dispose() + } + } + + + private var lastVisibilityState = View.VISIBLE + private val onShowLoaderChange: Consumer> = Consumer { + + Log.d(TAG, "showLoader: ${it.value}") + runOnUiThread { + if (it.value) { + supportActionBar?.hide() + binding.container.visibility = View.GONE + } else { + binding.container.visibility = View.VISIBLE + if (lastVisibilityState == View.VISIBLE) { + supportActionBar?.show() + } + } + } + } + + private val onIsAuthenticatedChange: Consumer> = Consumer { + Log.d(TAG, "isAuthenticated: ${it.value}") + runOnUiThread { + if (it.value) { + setupActionBarWithNavController(navController, authenticatedTabs) + navController.setGraph(R.navigation.navigation) + binding.navView.visibility = View.VISIBLE + setToolbarVisibility(true) + } else { + setupActionBarWithNavController(navController, nonAuthTabs) + navController.setGraph(R.navigation.not_auth_navigation) + binding.navView.visibility = View.GONE + setToolbarVisibility(false) + } + } + } + + + private fun setToolbarVisibility(visible: Boolean) { + lastVisibilityState = if (visible) View.VISIBLE else View.GONE + if (visible) { + supportActionBar?.show() + } else { + supportActionBar?.hide() + } + } + + +} \ No newline at end of file diff --git a/embedded/src/main/java/com/frontegg/demo/ui/home/HomeFragment.kt b/embedded/src/main/java/com/frontegg/demo/ui/home/HomeFragment.kt new file mode 100644 index 0000000..fb1ac51 --- /dev/null +++ b/embedded/src/main/java/com/frontegg/demo/ui/home/HomeFragment.kt @@ -0,0 +1,59 @@ +package com.frontegg.demo.ui.home + +import android.graphics.Color +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import com.bumptech.glide.Glide +import com.frontegg.android.FronteggAuth +import com.frontegg.demo.databinding.FragmentHomeBinding + +class HomeFragment : Fragment() { + + private var _binding: FragmentHomeBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val homeViewModel = ViewModelProvider(this)[HomeViewModel::class.java] + + _binding = FragmentHomeBinding.inflate(inflater, container, false) + + val root: View = binding.root + + binding.logoutButton.setBackgroundColor(Color.RED) + homeViewModel.user.observe(viewLifecycleOwner) { + + if (it != null) { + Glide.with(requireContext()).load(it.profilePictureUrl) + .into(binding.image) + binding.name.text = it.name + binding.email.text = it.email + binding.tenant.text = it.activeTenant.name + } + } + + binding.logoutButton.setOnClickListener { + FronteggAuth.instance.logout() + } + + return root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/embedded/src/main/java/com/frontegg/demo/ui/home/HomeViewModel.kt b/embedded/src/main/java/com/frontegg/demo/ui/home/HomeViewModel.kt new file mode 100644 index 0000000..22fe263 --- /dev/null +++ b/embedded/src/main/java/com/frontegg/demo/ui/home/HomeViewModel.kt @@ -0,0 +1,25 @@ +package com.frontegg.demo.ui.home + +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.frontegg.android.FronteggAuth +import com.frontegg.android.models.User + +class HomeViewModel : ViewModel() { + + private val _user = MutableLiveData().apply { + FronteggAuth.instance.user.subscribe { + Handler(Looper.getMainLooper()).post { + value = it.value + } + } + } + + + val user: LiveData = _user + + +} \ No newline at end of file diff --git a/embedded/src/main/java/com/frontegg/demo/ui/tenants/TenantsFragment.kt b/embedded/src/main/java/com/frontegg/demo/ui/tenants/TenantsFragment.kt new file mode 100644 index 0000000..cc591ee --- /dev/null +++ b/embedded/src/main/java/com/frontegg/demo/ui/tenants/TenantsFragment.kt @@ -0,0 +1,112 @@ +package com.frontegg.demo.ui.tenants + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.ListView +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import com.frontegg.android.FronteggAuth +import com.frontegg.android.models.Tenant +import com.frontegg.demo.R +import com.frontegg.demo.databinding.FragmentTenantsBinding + +class CustomAdapter(context: Context, private val dataSource: MutableList) : BaseAdapter() { + private val inflater: LayoutInflater = + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + + private var activeTenant: Tenant? = null + + + override fun getCount(): Int = dataSource.size + + override fun getItem(position: Int): Any = dataSource[position] + + override fun getItemId(position: Int): Long = position.toLong() + + fun setItems(items: List) { + this.dataSource.clear() + this.dataSource.addAll(items.sortedBy { it.name }) + notifyDataSetChanged() + } + + fun setActiveTenant(activeTenant: Tenant?) { + this.activeTenant = activeTenant + notifyDataSetChanged() + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { + val view = convertView ?: inflater.inflate(R.layout.tenant_row_item, parent, false) + val name: TextView = view.findViewById(R.id.name) + val info: TextView = view.findViewById(R.id.info) + + val item = getItem(position) as Tenant + name.text = item.name + + view.setOnClickListener { + info.text = " (switching...)" + info.visibility = View.VISIBLE + FronteggAuth.instance.switchTenant(item.tenantId) { + info.visibility = View.GONE + } + } + if (activeTenant?.tenantId == item.tenantId) { + info.text = " (active)" + info.visibility = View.VISIBLE + } else { + info.text = "" + info.visibility = View.GONE + } + + return view + } +} + +class TenantsFragment : Fragment() { + + private var _binding: FragmentTenantsBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + private var tenants: MutableList = mutableListOf() + + private lateinit var tenantsViewModel: TenantsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + tenantsViewModel = ViewModelProvider(this)[TenantsViewModel::class.java] + + _binding = FragmentTenantsBinding.inflate(inflater, container, false) + + val listView: ListView = binding.tenantsList + val adapter = CustomAdapter(requireContext(), tenants) + listView.adapter = adapter + + tenantsViewModel.tenants.observe(viewLifecycleOwner) { + adapter.setItems(it) + } + + tenantsViewModel.activeTenant.observe(viewLifecycleOwner) { + adapter.setActiveTenant(it) + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/embedded/src/main/java/com/frontegg/demo/ui/tenants/TenantsViewModel.kt b/embedded/src/main/java/com/frontegg/demo/ui/tenants/TenantsViewModel.kt new file mode 100644 index 0000000..684a117 --- /dev/null +++ b/embedded/src/main/java/com/frontegg/demo/ui/tenants/TenantsViewModel.kt @@ -0,0 +1,32 @@ +package com.frontegg.demo.ui.tenants + +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.frontegg.android.FronteggAuth +import com.frontegg.android.models.Tenant + +class TenantsViewModel : ViewModel() { + + + private val _tenants = MutableLiveData>().apply { + FronteggAuth.instance.user.subscribe { + Handler(Looper.getMainLooper()).post { + value = (it.value?.tenants ?: listOf()) + } + } + } + + private val _activeTenant = MutableLiveData().apply { + FronteggAuth.instance.user.subscribe { + Handler(Looper.getMainLooper()).post { + value = it.value?.activeTenant + } + } + } + + val tenants: LiveData> = _tenants + val activeTenant: LiveData = _activeTenant +} \ No newline at end of file diff --git a/embedded/src/main/res/drawable-v24/ic_launcher_foreground.xml b/embedded/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/embedded/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/embedded/src/main/res/drawable/baseline_person_24.xml b/embedded/src/main/res/drawable/baseline_person_24.xml new file mode 100644 index 0000000..8a1f52b --- /dev/null +++ b/embedded/src/main/res/drawable/baseline_person_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/embedded/src/main/res/drawable/ic_dashboard_black_24dp.xml b/embedded/src/main/res/drawable/ic_dashboard_black_24dp.xml new file mode 100644 index 0000000..46fc8de --- /dev/null +++ b/embedded/src/main/res/drawable/ic_dashboard_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/embedded/src/main/res/drawable/ic_home_black_24dp.xml b/embedded/src/main/res/drawable/ic_home_black_24dp.xml new file mode 100644 index 0000000..f8bb0b5 --- /dev/null +++ b/embedded/src/main/res/drawable/ic_home_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/embedded/src/main/res/drawable/ic_launcher_background.xml b/embedded/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/embedded/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/embedded/src/main/res/drawable/ic_notifications_black_24dp.xml b/embedded/src/main/res/drawable/ic_notifications_black_24dp.xml new file mode 100644 index 0000000..78b75c3 --- /dev/null +++ b/embedded/src/main/res/drawable/ic_notifications_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/embedded/src/main/res/layout/activity_navigation.xml b/embedded/src/main/res/layout/activity_navigation.xml new file mode 100644 index 0000000..b23fdb0 --- /dev/null +++ b/embedded/src/main/res/layout/activity_navigation.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/embedded/src/main/res/layout/fragment_auth.xml b/embedded/src/main/res/layout/fragment_auth.xml new file mode 100644 index 0000000..e0bd023 --- /dev/null +++ b/embedded/src/main/res/layout/fragment_auth.xml @@ -0,0 +1,30 @@ + + + + + +