Skip to content

Commit

Permalink
MBL-1167: New Login/Sign up Screen with Chrome Tabs (Phase 2) (#1941)
Browse files Browse the repository at this point in the history
* - ChromeTabIntent with CustomTabSession + CustomTabClient to be able to detect when the X button has been pressed, pre-render the url to reduce loading times, reload when no connectivity working

* - Documentation and some improvements

---------

Co-authored-by: mtgriego <matthew.t.griego@gmail.com>
  • Loading branch information
Arkariang and mtgriego committed Feb 1, 2024
1 parent a211737 commit 629a735
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 11 deletions.
3 changes: 2 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,13 @@ dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.annotation:annotation:1.3.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'androidx.browser:browser:1.4.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
// Chrome Tabs library
implementation 'androidx.browser:browser:1.7.0'

// Architecture components
def lifecycle_version = "2.2.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,21 @@ package com.kickstarter.models.chrome
// https://github.com/GoogleChrome/custom-tabs-client

import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.browser.customtabs.CustomTabsCallback
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsServiceConnection
import androidx.browser.customtabs.CustomTabsSession
import com.kickstarter.libs.utils.extensions.isNotNull
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import timber.log.Timber

class ChromeTabsHelperActivity {
companion object {
Expand Down Expand Up @@ -67,4 +79,85 @@ class ChromeTabsHelperActivity {
*/
fun openUri(activity: Activity, uri: Uri)
}

/**
* Helper class that will instantiate CustomTabsServiceConnection, once CustomTabsClient.bindCustomTabsService
* has been called in host activity `onCustomTabsServiceConnected` will
* provide the CustomTabsClient instance.
*
* CustomTabsClient will have onNavigationEvents callbacks, when detected TAB_HIDDEN
* navigation event, execute callback parameter
*
* Trying to call `getSession` before isSessionReady emission
* on `onCustomTabsServiceConnected` is true will result in null values.
*
* @param tabHiddenCallback the callback to be executed once the TAB_HIDDEN
* navigation event has been detected
*/
class CustomTabSessionAndClientHelper(
context: Context,
uri: Uri,
tabHiddenCallback: () -> Unit
) {
private var customClient: CustomTabsClient? = null
// - Do not expose mutable types
private var sessionReady: MutableSharedFlow<Boolean> = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val isSessionReady: Flow<Boolean> = sessionReady
private var session: CustomTabsSession? = null

private val callback = object : CustomTabsCallback() {
override fun onNavigationEvent(navigationEvent: Int, extras: Bundle?) {
Timber.d("onNavigationEvent: Code = $navigationEvent")
// - means the X button has been clicked, therefore ChromeTab has been dismissed
if (navigationEvent == TAB_HIDDEN) {
tabHiddenCallback()
}
}
}

private val connection: CustomTabsServiceConnection = object : CustomTabsServiceConnection() {
/**
* Using `CustomTabsClient.warmup` and `CustomTabsSession.mayLaunchUrl` will make Custom Tabs pre-fetch the page and pre-render.
* `CustomTabsClient.warmup` has no impact on performance
* `CustomTabsSession.mayLaunchUrl` comes with a network and battery cost, avoid on non critical user journeys.
*/
override fun onCustomTabsServiceConnected(
name: ComponentName,
client: CustomTabsClient
) {
customClient = client
customClient?.warmup(0)
session = customClient?.newSession(callback)
session?.mayLaunchUrl(uri, null, null)
sessionReady.tryEmit(session.isNotNull())
Timber.d("onCustomTabsServiceConnected")
}
override fun onServiceDisconnected(name: ComponentName) {
Timber.d("onServiceDisconnected")
customClient = null
session = null
}
}

init {
// - Bind the connection to the system service be able to listen to navigation events
CustomTabsClient.bindCustomTabsService(
context,
ChromeTabsHelper.getPackageNameToUse(context = context) ?: "",
connection
)
}

/**
* Trying to call `getSession` before isSessionReady emission
* on `onCustomTabsServiceConnected` is true will result in null values.
*/
fun getSession() = session
fun getConnection() = connection
/**
* Will emmit true once `onCustomTabsServiceConnected`
* has provided a new CustomTabsClient and the session is ready
*/
fun isSessionReady() = isSessionReady
}
}
42 changes: 35 additions & 7 deletions app/src/main/java/com/kickstarter/ui/activities/OAuthActivity.kt
Original file line number Diff line number Diff line change
@@ -1,24 +1,52 @@
package com.kickstarter.ui.activities

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import com.kickstarter.ui.extensions.finishWithAnimation
import androidx.browser.customtabs.CustomTabsIntent
import androidx.lifecycle.lifecycleScope
import com.kickstarter.libs.utils.TransitionUtils
import com.kickstarter.models.chrome.ChromeTabsHelperActivity
import com.kickstarter.ui.IntentKey
import com.kickstarter.ui.extensions.setUpConnectivityStatusCheck
import kotlinx.coroutines.launch

class OAuthActivity : AppCompatActivity() {

private lateinit var helper: ChromeTabsHelperActivity.CustomTabSessionAndClientHelper

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setUpConnectivityStatusCheck(lifecycle)

this.onBackPressedDispatcher.addCallback {
finishWithAnimation()
// TODO MBL-1168 the url will be retrieved from the VM,on MBL-1168 alongside PKCE paramenters
val uri = Uri.parse("https://www.kickstarter.com/oauth/authorizations")

// BindCustomTabsService, obtain CustomTabsClient and Client, listens to navigation events
helper = ChromeTabsHelperActivity.CustomTabSessionAndClientHelper(this, uri) {
finish()
}

// - Fallback in case Chrome is not installed, open WebViewActivity
val fallback = object : ChromeTabsHelperActivity.CustomTabFallback {
override fun openUri(activity: Activity, uri: Uri) {
val intent: Intent = Intent(activity, WebViewActivity::class.java)
.putExtra(IntentKey.URL, uri.toString())

activity.startActivity(intent)
TransitionUtils.slideInFromRight()
}
}

// TODO: Will continue work on https://kickstarter.atlassian.net/browse/MBL-1167
lifecycleScope.launch {
// - Once the session is ready and client warmed-up load the url
helper.isSessionReady().collect {
val tabIntent = CustomTabsIntent.Builder(helper.getSession()).build()
ChromeTabsHelperActivity.openCustomTab(this@OAuthActivity, tabIntent, uri, fallback)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,13 @@ fun Activity.startPreLaunchProjectActivity(project: Project, previousScreen: Str
fun Activity.startLogin(isOauthPathEnabled: Boolean) {
val intent = Intent().getStartLoginIntent(isOauthPathEnabled, this)
startActivityForResult(intent, ActivityRequestCodes.LOGIN_FLOW)
TransitionUtils.transition(this, TransitionUtils.fadeIn())
TransitionUtils.transition(this, TransitionUtils.slideInFromRight())
}

fun Activity.startSignup(isOauthPathEnabled: Boolean) {
val intent = Intent().getSignupIntent(isOauthPathEnabled, this)
startActivityForResult(intent, ActivityRequestCodes.LOGIN_FLOW)
TransitionUtils.transition(this, TransitionUtils.fadeIn())
TransitionUtils.transition(this, TransitionUtils.slideInFromRight())
}

fun Activity.startDisclaimerChromeTab(disclaimerItem: DisclaimerItems, environment: Environment?) {
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ buildscript {
}

dependencies {
classpath 'com.android.tools.build:gradle:8.2.0'
classpath 'com.android.tools.build:gradle:8.2.2'
classpath 'com.google.gms:google-services:4.3.15'
classpath "org.jacoco:org.jacoco.core:$jacoco_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
Expand Down

0 comments on commit 629a735

Please sign in to comment.