diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da2fa27..c945f47 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,7 +60,7 @@ jobs: uses: gradle/gradle-build-action@v2 - name: Build Libraries - run: ./gradlew build --no-daemon + run: ./gradlew :app:build --no-daemon # - name: AVD cache # uses: actions/cache@v3 # id: avd-cache diff --git a/README.md b/README.md index 6057007..cb0c26f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,27 @@ - ![Frontegg_Android_SDK (Kotlin)](./logo.png) Frontegg is a web platform where SaaS companies can set up their fully managed, scalable and brand aware - SaaS features and integrate them into their SaaS portals in up to 5 lines of code. -## Project Requirements +## Table of Contents + +- [Get Started](#get-started) + - [Project Requirements](#project-requirements) + - [Prepare Frontegg workspace](#prepare-frontegg-workspace) + - [Setup Hosted Login](#setup-hosted-login) + - [Add Frontegg package to the project](#add-frontegg-package-to-the-project) + - [Set minimum sdk version](#set-minimum-sdk-version) + - [Configure build config fields](#configure-build-config-fields) + - [Config Android AssetLinks](#config-android-assetlinks) +- [Usage](#usage) + - [Initialize FronteggApp](#initialize-fronteggapp) + - [Login with Frontegg](#login-with-frontegg) + - [Logout user](#logout) + - [Switch Tenant](#switch-tenant) + +## Get Started + +### Project Requirements - Android SDK 26+ Set defaultConfig's minSDK to 26+ in build.gradle: @@ -30,34 +47,27 @@ and integrate them into their SaaS portals in up to 5 lines of code. } ``` - -## Getting Started - - - [Prepare Frontegg workspace](#prepare-frontegg-workspace) - - [Setup Hosted Login](#setup-hosted-login) - - [Add frontegg package to the project](#add-frontegg-package-to-the-project) - - [Setup build variables](#setup-build-variables) - - [Initialize FronteggApp](#initialize-fronteggapp) - - [Add custom loading screen](#add-custom-loading-screen) - - [Config Android AssetLinks](#config-android-assetlinks) - ### Prepare Frontegg workspace Navigate to [Frontegg Portal Settings](https://portal.frontegg.com/development/settings), If you don't have application follow integration steps after signing up. -Copy FronteggDomain to future steps from [Frontegg Portal Domain](https://portal.frontegg.com/development/settings/domains) +Copy FronteggDomain to future steps +from [Frontegg Portal Domain](https://portal.frontegg.com/development/settings/domains) ### Setup Hosted Login - Navigate to [Login Method Settings](https://portal.frontegg.com/development/authentication/hosted) - Toggle Hosted login method -- Add `{{LOGIN_URL}}/mobile/callback` +- Add `{{ANDROID_PACKAGE_NAME}}://{{FRONTEGG_BASE_URL}}/ios/oauth/callback` +- Replace `ANDROID_PACKAGE_NAME` with your application identifier +- Replace `FRONTEGG_BASE_URL` with your Frontegg base url -### Add frontegg package to the project +### Add Frontegg package to the project - Open you project - Find your app's build.gradle file -- Add the following to your dependencies section: +- Add the following to your dependencies section: + ```groovy dependencies { // Add the Frontegg Android Kotlin SDK @@ -65,34 +75,123 @@ Copy FronteggDomain to future steps from [Frontegg Portal Domain](https://portal } ``` -### Setup build variables +### Set minimum sdk version + +To set up your Android minimum sdk version, open root gradle file at`android/build.gradle`, +and add/edit the `minSdkVersion` under `buildscript.ext`: + +```groovy +buildscript { + ext { + minSdkVersion = 26 + // ... + } +} +``` + +### Configure build config fields -To setup your Android application to communicate with Frontegg, you have to use `manifestPlaceholders` property in your build.gradle -file, this property will store frontegg hostname (without https) and client id from previous step: +To set up your Android application on to communicate with Frontegg, you have to add `buildConfigField` property the +gradle `android/app/build.gradle`. +This property will store frontegg hostname (without https) and client id from previous step: ```groovy -def fronteggDomain = "DOMAIN_HOST.com" -def fronteggClientId = "CLIENT_ID" +def fronteggDomain = "FRONTEGG_DOMAIN_HOST.com" // without protocol https:// +def fronteggClientId = "FRONTEGG_CLIENT_ID" android { defaultConfig { + manifestPlaceholders = [ + "package_name" : applicationId, "frontegg_domain" : fronteggDomain, - frontegg_client_id: fronteggClientId + "frontegg_client_id": fronteggClientId ] buildConfigField "String", 'FRONTEGG_DOMAIN', "\"$fronteggDomain\"" buildConfigField "String", 'FRONTEGG_CLIENT_ID', "\"$fronteggClientId\"" } - /* Use buildTypes for release / debug configurations */ - // ... + + +} +``` + +Add bundleConfig=true if not exists inside the android section inside the app gradle `android/app/build.gradle` + +```groovy +android { + buildFeatures { + buildConfig = true + } +} +``` + +### Permissions + +Add `INTERNET` permission to the app's manifest file. + +```xml + + + +``` + +### Config Android AssetLinks + +Configuring your Android `AssetLinks` is required for Magic Link authentication / Reset Password / Activate Account / +login with IdPs. + +To add your `AssetLinks` to your Frontegg application, you will need to update in each of your integrated Frontegg +Environments the `AssetLinks` that you would like to use with that Environment. Send a POST request +to `https://api.frontegg.com/vendors/resources/associated-domains/v1/android` with the following payload: + +``` +{ + "packageName": "YOUR_APPLICATION_PACKAGE_NAME", + "sha256CertFingerprints": ["YOUR_KEYSTORE_CERT_FINGERPRINTS"] } ``` +Each Android app has multiple certificate fingerprint, to get your `DEBUG` sha256CertFingerprint you have to run the +following command: + +For Debug mode, run the following command and copy the `SHA-256` value + +NOTE: make sure to choose the Variant and Config equals to `debug` + +```bash +./gradlew signingReport + +################### +# Example Output: +################### + +# Variant: debug +# Config: debug +# Store: /Users/davidfrontegg/.android/debug.keystore +# Alias: AndroidDebugKey +# MD5: 25:F5:99:23:FC:12:CA:10:8C:43:F4:02:7D:AD:DC:B6 +# SHA1: FC:3C:88:D6:BF:4E:62:2E:F0:24:1D:DB:D7:15:36:D6:3E:14:84:50 +# SHA-256: D9:6B:4A:FD:62:45:81:65:98:4D:5C:8C:A0:68:7B:7B:A5:31:BD:2B:9B:48:D9:CF:20:AE:56:FD:90:C1:C5:EE +# Valid until: Tuesday, 18 June 2052 + +``` + +For Release mode, Extract the SHA256 using keytool from your `Release` keystore file: + +```bash +keytool -list -v -keystore /PATH/file.jks -alias YourAlias -storepass *** -keypass *** +``` + +In order to use our API’s, follow [this guide](https://docs.frontegg.com/reference/getting-started-with-your-api) to +generate a vendor token. + +## Usage + ### Initialize FronteggApp -Create a custom `App` class that extends `android.app.Application` to initialize `FronteggApp` +Create a custom `App` class that extends `android.app.Application` to initialize `FronteggApp`: ```kotlin package com.frontegg.demo @@ -109,7 +208,6 @@ class App : Application() { BuildConfig.FRONTEGG_DOMAIN, BuildConfig.FRONTEGG_CLIENT_ID, this, // Application Context - null ) } } @@ -118,208 +216,89 @@ class App : Application() { Register the custom `App` in the app's manifest file **AndroidManifest.xml:** + ```xml - > + android:name=".App"> ``` -android:name=".App" -### Add Frontegg Activity +android:name=".App" -Create `FronteggActivity` class that extends `com.frontegg.android.AbstractFronteggActivity` to handle authenticated redirects +## Login with Frontegg -**FronteggAcivity.kt:** +In order to login with Frontegg, you have to call `FronteggAuth.instance.login` method with `activtity` context. +Login method will open Frontegg hosted login page, and will return user data after successful login. ```kotlin -package com.frontegg.demo - -import android.content.Intent -import com.frontegg.android.AbstractFronteggActivity -class FronteggActivity: AbstractFronteggActivity() { +import com.frontegg.android.FronteggAuth - /** - * This function will be called everytime the FronteggActivity - * successfully authenticated and should redirect the user to - * authenticated screens - * - * NOTE: Replace the `MainActivity::class.java` with your Main Activity class - */ - override fun navigateToAuthenticated() { - val intent = Intent(this, MainActivity::class.java) - startActivity(intent) - finish() +class FirstFragment : Fragment() { + // ... + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + + binding.loginButton.setOnClickListener { + FronteggAuth.instance.login(requireActivity()) } + } + // ... } -``` - -Register the `FronteggActivity` in the app's manifest file -**AndroidManifest.xml:** -```xml - - - - - - - - - - - - - - - ``` -### Add Frontegg Logout Activity - +## Logout user -Create `FronteggLogoutActivity` class that extends `com.frontegg.android.AbstractFronteggLogoutActivity` to handle logout redirects - -**FronteggLogoutActivity.kt:** +In order to logout user, you have to call `FronteggAuth.instance.logout` method. +Logout method will clear all user data from the device. ```kotlin -package com.frontegg.demo -import android.content.Intent -import com.frontegg.android.AbstractFronteggLogoutActivity +import com.frontegg.android.FronteggAuth -class FronteggLogoutActivity: AbstractFronteggLogoutActivity() { - /** - * This function will be called everytime the FronteggLogoutActivity - * successfully logged out and should redirect the user to - * your the previous created `FronteggActivity` - * - * NOTE: Replace the `FronteggActivity::class.java` with your `FronteggActivity` class - */ - override fun navigateToFronteggLogin() { - val intent = Intent(this, FronteggActivity::class.java) - startActivity(intent) - finish() +class FirstFragment : Fragment() { + // ... + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + + binding.logoutButton.setOnClickListener { + FronteggAuth.instance.logout() } + } + // ... } -``` -Register the `FronteggLogoutActivity` in the app's manifest file - -**AndroidManifest.xml:** -```xml - ``` -### Add custom loading screen -In order to customize Frontegg loading screen: +## Switch Tenant -- Create new layout file contains your loader screen design: -```xml - - - - - - - - -``` +In order to switch tenant, you have to call `FronteggAuth.instance.switchTenant` method with `activtity` context. -- Add `R.layout.loader` FronteggApp.init as the 4th argument: ```kotlin - package com.frontegg.demo + +import com.frontegg.android.FronteggAuth + +class FirstFragment : Fragment() { + // ... - import android.app.Application - import com.frontegg.android.FronteggApp + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - class App : Application() { + val tenantIds = FronteggAuth.instance.user.value?.tenantIds ?: listOf() - override fun onCreate() { - super.onCreate() - - FronteggApp.init( - BuildConfig.FRONTEGG_DOMAIN, - BuildConfig.FRONTEGG_CLIENT_ID, - this, - R.layout.loader // <<-- here - ) - } + /** + * pick one from `tenantIds` list: + */ + val tenantToSwitchTo = tenantIds[0] + + binding.switchTenant.setOnClickListener { + FronteggAuth.instance.switchTenant(tenantToSwitchTo) + } } -``` - -### Permissions - -Add `INTERNET` permission to the app's manifest file. - -```xml - -``` - - -### Config Android AssetLinks -Configuring your Android `AssetLinks` is required for Magic Link authentication / Reset Password / Activate Account / login with IdPs. - -To add your `AssetLinks` to your Frontegg application, you will need to update in each of your integrated Frontegg Environments the `AssetLinks` that you would like to use with that Environment. Send a POST request to `https://api.frontegg.com/vendors/resources/associated-domains/v1/android` with the following payload: -``` -{ - "packageName": "YOUR_APPLICATION_PACKAGE_NAME", - "sha256CertFingerprints": ["YOUR_KEYSTORE_CERT_FINGERPRINTS"] + // ... } -``` - -Each Android app has multiple certificate fingerprint, to get your `DEBUG` sha256CertFingerprint you have to run the following command: - -For Debug mode, run the following command and copy the `SHA-256` value -```bash -./gradlew signingReport - -################### -# Example Output: -################### - -# Variant: debugAndroidTest -# Config: debug -# Store: /Users/davidfrontegg/.android/debug.keystore -# Alias: AndroidDebugKey -# MD5: 25:F5:99:23:FC:12:CA:10:8C:43:F4:02:7D:AD:DC:B6 -# SHA1: FC:3C:88:D6:BF:4E:62:2E:F0:24:1D:DB:D7:15:36:D6:3E:14:84:50 -# SHA-256: D9:6B:4A:FD:62:45:81:65:98:4D:5C:8C:A0:68:7B:7B:A5:31:BD:2B:9B:48:D9:CF:20:AE:56:FD:90:C1:C5:EE -# Valid until: Tuesday, 18 June 2052 ``` - - -For Release mode, Extract the SHA256 using keytool from your `Release` keystore file: -```bash -keytool -list -v -keystore /PATH/file.jks -alias YourAlias -storepass *** -keypass *** -``` - -In order to use our API’s, follow [this guide](https://docs.frontegg.com/reference/getting-started-with-your-api) to generate a vendor token. diff --git a/android/build.gradle b/android/build.gradle index 2c7631c..7512564 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,15 +6,16 @@ plugins { } group 'com.frontegg.android' -version '1.0.4' +version '1.0.6' + android { namespace 'com.frontegg.android' - compileSdk 33 + compileSdk 34 defaultConfig { minSdk 26 - targetSdk 33 + targetSdk 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" versionName "$version" @@ -48,11 +49,12 @@ android { dependencies { implementation 'androidx.core:core-ktx:[1.7,)' implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1' - implementation 'com.squareup.okhttp3:okhttp:4.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.2' implementation 'com.google.code.gson:gson:2.8.9' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1" - implementation "androidx.browser:browser:1.5.0" + 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' } afterEvaluate { diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index a827463..4a0f89a 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -2,9 +2,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/src/main/java/com/frontegg/android/FronteggAuth.kt b/android/src/main/java/com/frontegg/android/FronteggAuth.kt index 31a2f4b..b104fcc 100644 --- a/android/src/main/java/com/frontegg/android/FronteggAuth.kt +++ b/android/src/main/java/com/frontegg/android/FronteggAuth.kt @@ -114,8 +114,6 @@ class FronteggAuth( refreshTaskRunner?.cancel() - - if (decoded.exp > 0) { val now: Long = Instant.now().toEpochMilli() val offset = (((decoded.exp * 1000) - now) * 0.80).toLong() @@ -166,6 +164,7 @@ class FronteggAuth( credentialManager.clear() val handler = Handler(Looper.getMainLooper()) handler.post { + isLoading.value = false callback() } } @@ -176,4 +175,20 @@ class FronteggAuth( fun login(activity: Activity) { AuthenticationActivity.authenticateUsingBrowser(activity) } + + + fun switchTenant(tenantId: String, callback: () -> Unit = {}) { + GlobalScope.launch(Dispatchers.IO) { + + isLoading.value = true + api.switchTenant(tenantId) + refreshTokenIfNeeded() + + val handler = Handler(Looper.getMainLooper()) + handler.post { + isLoading.value = false + callback() + } + } + } } diff --git a/android/src/main/java/com/frontegg/android/models/Tenant.kt b/android/src/main/java/com/frontegg/android/models/Tenant.kt new file mode 100644 index 0000000..9647a9d --- /dev/null +++ b/android/src/main/java/com/frontegg/android/models/Tenant.kt @@ -0,0 +1,15 @@ +package com.frontegg.android.models + +class Tenant { + public lateinit var id: String + public lateinit var name: String + public var creatorEmail: String? = null + public var creatorName: String? = null + public lateinit var tenantId: String + public lateinit var createdAt: String + public lateinit var updatedAt: String + public var isReseller: Boolean = false + public lateinit var metadata: String + public lateinit var vendorId: String + public var website: String? = null +} diff --git a/android/src/main/java/com/frontegg/android/models/User.kt b/android/src/main/java/com/frontegg/android/models/User.kt index 16b4da2..7e043fc 100644 --- a/android/src/main/java/com/frontegg/android/models/User.kt +++ b/android/src/main/java/com/frontegg/android/models/User.kt @@ -12,6 +12,8 @@ class User { public lateinit var permissions: List public lateinit var tenantId: String public lateinit var tenantIds: List + public lateinit var tenants: List + public lateinit var activeTenant: Tenant public var activatedForTenant: Boolean = false public var metadata: String? = null public var verified: Boolean = false diff --git a/android/src/main/java/com/frontegg/android/services/Api.kt b/android/src/main/java/com/frontegg/android/services/Api.kt index 21163d0..e0a0870 100644 --- a/android/src/main/java/com/frontegg/android/services/Api.kt +++ b/android/src/main/java/com/frontegg/android/services/Api.kt @@ -6,6 +6,7 @@ import com.frontegg.android.utils.ApiConstants import com.frontegg.android.utils.CredentialKeys import com.google.gson.Gson import com.google.gson.JsonObject +import com.google.gson.reflect.TypeToken import okhttp3.Call import okhttp3.Headers import okhttp3.Headers.Companion.toHeaders @@ -67,6 +68,26 @@ open class Api( return this.httpClient.newCall(request) } + private fun buildPutRequest( + path: String, + body: JsonObject, + additionalHeaders: Map = mapOf() + ): Call { + val url = "${this.baseUrl}/$path".toHttpUrl() + val requestBuilder = Request.Builder() + val bodyRequest = + body.toString() + .toRequestBody("application/json; charset=utf-8".toMediaType()) + val headers = this.prepareHeaders(additionalHeaders); + + requestBuilder.method("PUT", bodyRequest) + requestBuilder.headers(headers); + requestBuilder.url(url) + + val request = requestBuilder.build() + return this.httpClient.newCall(request) + } + private fun buildGetRequest(path: String): Call { val url = "$baseUrl/$path".toHttpUrl() val requestBuilder = Request.Builder() @@ -82,10 +103,27 @@ open class Api( @Throws(IllegalArgumentException::class, IOException::class) public fun me(): User? { - val call = buildGetRequest(ApiConstants.me) - val response = call.execute() - if (response.isSuccessful) { - return Gson().fromJson(response.body!!.string(), User::class.java) + val meCall = buildGetRequest(ApiConstants.me) + val meResponse = meCall.execute() + val tenantsCall = buildGetRequest(ApiConstants.tenants) + val tenantsResponse = tenantsCall.execute() + + if (meResponse.isSuccessful && tenantsResponse.isSuccessful) { + // Parsing JSON strings into JsonObject + val gson = Gson() + val mapType = object : TypeToken>() {}.type + + val meJsonStr = meResponse.body!!.string() + val tenantsJsonStr = tenantsResponse.body!!.string() + + val meJson: MutableMap = gson.fromJson(meJsonStr, mapType) + val tenantsJson: MutableMap = gson.fromJson(tenantsJsonStr, mapType) + + meJson["tenants"] = tenantsJson["tenants"] as Any + meJson["activeTenant"] = tenantsJson["activeTenant"] as Any + + val merged = Gson().toJson(meJson) + return Gson().fromJson(merged, User::class.java) } return null @@ -142,4 +180,11 @@ open class Api( call.execute() } } + + fun switchTenant(tenantId: String) { + val data = JsonObject() + data.addProperty("tenantId", tenantId) + val call = buildPutRequest(ApiConstants.switchTenant, data) + call.execute() + } } \ No newline at end of file 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 7013f84..3c79a47 100644 --- a/android/src/main/java/com/frontegg/android/utils/Constants.kt +++ b/android/src/main/java/com/frontegg/android/utils/Constants.kt @@ -2,12 +2,15 @@ package com.frontegg.android.utils import com.frontegg.android.FronteggApp + class ApiConstants { companion object { const val me: String = "identity/resources/users/v2/me" + const val tenants: String = "identity/resources/users/v3/me/tenants" const val refreshToken: String = "oauth/token" const val exchangeToken: String = "oauth/token" const val logout: String = "identity/resources/auth/v1/logout" + const val switchTenant: String = "identity/resources/users/v1/tenant" } } @@ -16,7 +19,11 @@ class Constants { companion object { fun oauthCallbackUrl(baseUrl: String): String { - return "$baseUrl/android/${FronteggApp.getInstance().packageName}/callback" + + val host = baseUrl.substring("https://".length) + val packageName = FronteggApp.getInstance().packageName + + return "${packageName}://${host}/android/oauth/callback" } } diff --git a/android/src/main/java/com/frontegg/android/utils/ObservableValue.kt b/android/src/main/java/com/frontegg/android/utils/ObservableValue.kt index 2fa81c9..521edd6 100644 --- a/android/src/main/java/com/frontegg/android/utils/ObservableValue.kt +++ b/android/src/main/java/com/frontegg/android/utils/ObservableValue.kt @@ -1,5 +1,7 @@ package com.frontegg.android.utils +import android.os.Handler +import android.os.Looper import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.functions.Consumer import io.reactivex.rxjava3.subjects.PublishSubject @@ -25,6 +27,7 @@ class ObservableValue(value: T) { } fun subscribe(onNext: Consumer>): Disposable { + observable.subscribe() val disposable = observable.subscribe(onNext) onNext.accept(nullableObject) return disposable diff --git a/app/build.gradle b/app/build.gradle index 538a4b1..9242747 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,7 +8,7 @@ def fronteggClientId = "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca" android { namespace 'com.frontegg.demo' - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "com.frontegg.demo" @@ -61,9 +61,10 @@ dependencies { implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' implementation 'androidx.tracing:tracing:1.1.0' implementation project(path: ':android') - - - + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' + 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' androidTestImplementation 'org.jetbrains.kotlin:kotlin-stdlib:' + rootProject.kotlinVersion androidTestImplementation 'androidx.test:core:' + rootProject.coreVersion diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cc6e298..6f7c3fd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,36 +15,18 @@ android:supportsRtl="true" android:theme="@style/Theme.MyApplication" tools:targetApi="31"> - + android:label="@string/title_activity_navigation" > + - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/frontegg/demo/App.kt b/app/src/main/java/com/frontegg/demo/App.kt index 3dbdf9a..cfd1aa8 100644 --- a/app/src/main/java/com/frontegg/demo/App.kt +++ b/app/src/main/java/com/frontegg/demo/App.kt @@ -5,12 +5,17 @@ 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 + this, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/frontegg/demo/FirstFragment.kt b/app/src/main/java/com/frontegg/demo/AuthFragment.kt similarity index 66% rename from app/src/main/java/com/frontegg/demo/FirstFragment.kt rename to app/src/main/java/com/frontegg/demo/AuthFragment.kt index 1918665..f5328c3 100644 --- a/app/src/main/java/com/frontegg/demo/FirstFragment.kt +++ b/app/src/main/java/com/frontegg/demo/AuthFragment.kt @@ -1,22 +1,22 @@ package com.frontegg.demo -import android.content.Intent +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.navigation.fragment.findNavController +import androidx.appcompat.app.AppCompatActivity import com.frontegg.android.FronteggAuth -import com.frontegg.demo.databinding.FragmentFirstBinding +import com.frontegg.demo.databinding.FragmentAuthBinding import io.reactivex.rxjava3.disposables.Disposable /** * A simple [Fragment] subclass as the default destination in the navigation. */ -class FirstFragment : Fragment() { +class AuthFragment : Fragment() { - private var _binding: FragmentFirstBinding? = null + private var _binding: FragmentAuthBinding? = null // This property is only valid between onCreateView and // onDestroyView. @@ -28,7 +28,7 @@ class FirstFragment : Fragment() { savedInstanceState: Bundle? ): View { - _binding = FragmentFirstBinding.inflate(inflater, container, false) + _binding = FragmentAuthBinding.inflate(inflater, container, false) return binding.root } @@ -36,22 +36,10 @@ class FirstFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - binding.logoutButton.setOnClickListener { - - FronteggAuth.instance.logout() - } - binding.loginButton.setOnClickListener { FronteggAuth.instance.login(requireActivity()) } - - disposables.add(FronteggAuth.instance.user.subscribe { - activity?.runOnUiThread { - binding.textviewFirst.text = it.value?.email - } - }) } override fun onDestroyView() { diff --git a/app/src/main/java/com/frontegg/demo/FronteggActivity.kt b/app/src/main/java/com/frontegg/demo/FronteggActivity.kt deleted file mode 100644 index 8dd6ed4..0000000 --- a/app/src/main/java/com/frontegg/demo/FronteggActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.frontegg.demo - - -class FronteggActivity() { -// fun navigateToAuthenticated() { -// val intent = Intent(this, MainActivity::class.java) -// intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) -//// startActivity(intent) -//// finish() -// } -} \ No newline at end of file diff --git a/app/src/main/java/com/frontegg/demo/MainActivity.kt b/app/src/main/java/com/frontegg/demo/MainActivity.kt deleted file mode 100644 index e79ba01..0000000 --- a/app/src/main/java/com/frontegg/demo/MainActivity.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.frontegg.demo - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.util.Log -import android.view.Menu -import android.view.MenuItem -import androidx.appcompat.app.AppCompatActivity -import androidx.navigation.findNavController -import androidx.navigation.ui.AppBarConfiguration -import androidx.navigation.ui.navigateUp -import androidx.navigation.ui.setupActionBarWithNavController -import com.frontegg.android.utils.AuthorizeUrlGenerator -import com.frontegg.demo.databinding.ActivityMainBinding -import com.google.android.material.snackbar.Snackbar - -class MainActivity : AppCompatActivity() { - - private lateinit var appBarConfiguration: AppBarConfiguration - private lateinit var binding: ActivityMainBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - - - setSupportActionBar(binding.toolbar) - - val navController = findNavController(R.id.nav_host_fragment_content_main) - appBarConfiguration = AppBarConfiguration(navController.graph) - setupActionBarWithNavController(navController, appBarConfiguration) - - binding.fab.setOnClickListener { view -> - Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG).show() - - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - // Inflate the menu; this adds items to the action bar if it is present. - menuInflater.inflate(R.menu.menu_main, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - return when (item.itemId) { - R.id.action_settings -> true - else -> super.onOptionsItemSelected(item) - } - } - - override fun onSupportNavigateUp(): Boolean { - val navController = findNavController(R.id.nav_host_fragment_content_main) - return navController.navigateUp(appBarConfiguration) - || super.onSupportNavigateUp() - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/frontegg/demo/NavigationActivity.kt b/app/src/main/java/com/frontegg/demo/NavigationActivity.kt new file mode 100644 index 0000000..dedcd82 --- /dev/null +++ b/app/src/main/java/com/frontegg/demo/NavigationActivity.kt @@ -0,0 +1,103 @@ +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.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) + fronteggAuth.showLoader.subscribe(this.onShowLoaderChange) + fronteggAuth.isAuthenticated.subscribe(this.onIsAuthenticatedChange) + } + + 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/app/src/main/java/com/frontegg/demo/SecondFragment.kt b/app/src/main/java/com/frontegg/demo/SecondFragment.kt deleted file mode 100644 index 67da33b..0000000 --- a/app/src/main/java/com/frontegg/demo/SecondFragment.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.frontegg.demo - -import android.os.Bundle -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.navigation.fragment.findNavController -import com.frontegg.demo.databinding.FragmentSecondBinding - -/** - * A simple [Fragment] subclass as the second destination in the navigation. - */ -class SecondFragment : Fragment() { - - private var _binding: FragmentSecondBinding? = 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 { - - _binding = FragmentSecondBinding.inflate(inflater, container, false) - return binding.root - - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.buttonSecond.setOnClickListener { - findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment) - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/frontegg/demo/ui/home/HomeFragment.kt b/app/src/main/java/com/frontegg/demo/ui/home/HomeFragment.kt new file mode 100644 index 0000000..fb1ac51 --- /dev/null +++ b/app/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/app/src/main/java/com/frontegg/demo/ui/home/HomeViewModel.kt b/app/src/main/java/com/frontegg/demo/ui/home/HomeViewModel.kt new file mode 100644 index 0000000..22fe263 --- /dev/null +++ b/app/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/app/src/main/java/com/frontegg/demo/ui/tenants/TenantsFragment.kt b/app/src/main/java/com/frontegg/demo/ui/tenants/TenantsFragment.kt new file mode 100644 index 0000000..98a433f --- /dev/null +++ b/app/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?.id == item.id) { + 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/app/src/main/java/com/frontegg/demo/ui/tenants/TenantsViewModel.kt b/app/src/main/java/com/frontegg/demo/ui/tenants/TenantsViewModel.kt new file mode 100644 index 0000000..684a117 --- /dev/null +++ b/app/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/app/src/main/res/drawable/baseline_person_24.xml b/app/src/main/res/drawable/baseline_person_24.xml new file mode 100644 index 0000000..8a1f52b --- /dev/null +++ b/app/src/main/res/drawable/baseline_person_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_dashboard_black_24dp.xml b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml new file mode 100644 index 0000000..46fc8de --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_black_24dp.xml b/app/src/main/res/drawable/ic_home_black_24dp.xml new file mode 100644 index 0000000..f8bb0b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml new file mode 100644 index 0000000..78b75c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 07f9675..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_navigation.xml b/app/src/main/res/layout/activity_navigation.xml new file mode 100644 index 0000000..b23fdb0 --- /dev/null +++ b/app/src/main/res/layout/activity_navigation.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml deleted file mode 100644 index e416e1c..0000000 --- a/app/src/main/res/layout/content_main.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_second.xml b/app/src/main/res/layout/fragment_auth.xml similarity index 70% rename from app/src/main/res/layout/fragment_second.xml rename to app/src/main/res/layout/fragment_auth.xml index bd90524..e0bd023 100644 --- a/app/src/main/res/layout/fragment_second.xml +++ b/app/src/main/res/layout/fragment_auth.xml @@ -4,24 +4,27 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".SecondFragment"> + tools:context=".AuthFragment">