Commit cd71b275 authored by Filipe de Lima Brito's avatar Filipe de Lima Brito

Add login with Gitlab (OAuth).

parent 93db630c
...@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME ...@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
...@@ -50,6 +50,11 @@ ...@@ -50,6 +50,11 @@
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity
android:name=".webview.gitlab.ui.GitlabWebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity <activity
android:name=".chatroom.ui.ChatRoomActivity" android:name=".chatroom.ui.ChatRoomActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
......
...@@ -37,7 +37,7 @@ interface LoginView : LoadingView, MessageView, InternetView { ...@@ -37,7 +37,7 @@ interface LoginView : LoadingView, MessageView, InternetView {
/** /**
* Shows the CAS button if the sign in/sign out via CAS protocol is enabled by the server settings. * Shows the CAS button if the sign in/sign out via CAS protocol is enabled by the server settings.
* *
* REMARK: We must set up the CAS button listener [setupCasButtonListener]. * REMARK: We must set up the CAS button listener before showing it [setupCasButtonListener].
*/ */
fun showCasButton() fun showCasButton()
...@@ -49,8 +49,8 @@ interface LoginView : LoadingView, MessageView, InternetView { ...@@ -49,8 +49,8 @@ interface LoginView : LoadingView, MessageView, InternetView {
/** /**
* Setups the CAS button when tapped. * Setups the CAS button when tapped.
* *
* @param casUrl The CAS URL to login/sign up with. * @param casUrl The CAS URL to authenticate with.
* @param casToken The requested Token sent to the CAS server. * @param casToken The requested token to be sent to the CAS server.
*/ */
fun setupCasButtonListener(casUrl: String, casToken: String) fun setupCasButtonListener(casUrl: String, casToken: String)
...@@ -96,40 +96,60 @@ interface LoginView : LoadingView, MessageView, InternetView { ...@@ -96,40 +96,60 @@ interface LoginView : LoadingView, MessageView, InternetView {
fun hideLoginButton() fun hideLoginButton()
/** /**
* Shows the "login by Facebook view if it is enabled by the server settings. * Shows the "login by Facebook view if it is enable by the server settings.
*/ */
fun enableLoginByFacebook() fun enableLoginByFacebook()
/** /**
* Shows the "login by Github" view if it is enabled by the server settings. * Shows the "login by Github" view if it is enable by the server settings.
*
* REMARK: We must set up the Github button listener before enabling it [setupGithubButtonListener].
*/ */
fun enableLoginByGithub() fun enableLoginByGithub()
/** /**
* Shows the "login by Google" view if it is enabled by the server settings. * Setups the Github button when tapped.
*
* @param githubUrl The Github OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGithubButtonListener(githubUrl: String, state: String)
/**
* Shows the "login by Google" view if it is enable by the server settings.
*/ */
fun enableLoginByGoogle() fun enableLoginByGoogle()
/** /**
* Shows the "login by Linkedin" view if it is enabled by the server settings. * Shows the "login by Linkedin" view if it is enable by the server settings.
*/ */
fun enableLoginByLinkedin() fun enableLoginByLinkedin()
/** /**
* Shows the "login by Meteor" view if it is enabled by the server settings. * Shows the "login by Meteor" view if it is enable by the server settings.
*/ */
fun enableLoginByMeteor() fun enableLoginByMeteor()
/** /**
* Shows the "login by Twitter" view if it is enabled by the server settings. * Shows the "login by Twitter" view if it is enable by the server settings.
*/ */
fun enableLoginByTwitter() fun enableLoginByTwitter()
/** /**
* Shows the "login by Gitlab" view if it is enabled by the server settings. * Shows the "login by Gitlab" view if it is enable by the server settings.
*
* REMARK: We must set up the Gitlab button listener before enabling it [setupGitlabButtonListener].
*/ */
fun enableLoginByGitlab() fun enableLoginByGitlab()
/**
* Setups the Gitlab button when tapped.
*
* @param gitlabUrl The Gitlab OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGitlabButtonListener(gitlabUrl: String, state: String)
/** /**
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)). * Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
*/ */
......
...@@ -19,12 +19,17 @@ import chat.rocket.android.authentication.login.presentation.LoginView ...@@ -19,12 +19,17 @@ import chat.rocket.android.authentication.login.presentation.LoginView
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.webview.cas.ui.webViewIntent import chat.rocket.android.webview.cas.ui.INTENT_CAS_TOKEN
import chat.rocket.android.webview.cas.ui.casWebViewIntent
import chat.rocket.android.webview.gitlab.ui.INTENT_OAUTH_CREDENTIAL_SECRET
import chat.rocket.android.webview.gitlab.ui.INTENT_OAUTH_CREDENTIAL_TOKEN
import chat.rocket.android.webview.gitlab.ui.gitlabWebViewIntent
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.* import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject import javax.inject.Inject
internal const val REQUEST_CODE_FOR_CAS = 1 internal const val REQUEST_CODE_FOR_CAS = 1
internal const val REQUEST_CODE_FOR_OAUTH = 2
class LoginFragment : Fragment(), LoginView { class LoginFragment : Fragment(), LoginView {
@Inject lateinit var presenter: LoginPresenter @Inject lateinit var presenter: LoginPresenter
...@@ -64,10 +69,14 @@ class LoginFragment : Fragment(), LoginView { ...@@ -64,10 +69,14 @@ class LoginFragment : Fragment(), LoginView {
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_FOR_CAS) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_FOR_CAS) {
data?.apply {
presenter.authenticateWithCas(getStringExtra(INTENT_CAS_TOKEN))
}
} else if (requestCode == REQUEST_CODE_FOR_OAUTH) {
data?.apply { data?.apply {
presenter.authenticateWithCas(getStringExtra("cas_token")) presenter.authenticateWithOauth(getStringExtra(INTENT_OAUTH_CREDENTIAL_TOKEN), getStringExtra(INTENT_OAUTH_CREDENTIAL_SECRET))
} }
} }
} }
...@@ -121,7 +130,7 @@ class LoginFragment : Fragment(), LoginView { ...@@ -121,7 +130,7 @@ class LoginFragment : Fragment(), LoginView {
override fun setupLoginButtonListener() { override fun setupLoginButtonListener() {
button_log_in.setOnClickListener { button_log_in.setOnClickListener {
presenter.authenticate(text_username_or_email.textContent, text_password.textContent) presenter.authenticateWithUserAndPassword(text_username_or_email.textContent, text_password.textContent)
} }
} }
...@@ -147,7 +156,7 @@ class LoginFragment : Fragment(), LoginView { ...@@ -147,7 +156,7 @@ class LoginFragment : Fragment(), LoginView {
override fun setupCasButtonListener(casUrl: String, casToken: String) { override fun setupCasButtonListener(casUrl: String, casToken: String) {
button_cas.setOnClickListener { button_cas.setOnClickListener {
startActivityForResult(context?.webViewIntent(casUrl, casToken), REQUEST_CODE_FOR_CAS) startActivityForResult(context?.casWebViewIntent(casUrl, casToken), REQUEST_CODE_FOR_CAS)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold) activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
} }
} }
...@@ -192,31 +201,46 @@ class LoginFragment : Fragment(), LoginView { ...@@ -192,31 +201,46 @@ class LoginFragment : Fragment(), LoginView {
} }
override fun enableLoginByFacebook() { override fun enableLoginByFacebook() {
button_facebook.isEnabled = true button_facebook.isClickable = true
} }
override fun enableLoginByGithub() { override fun enableLoginByGithub() {
button_github.isEnabled = true button_github.isClickable = true
}
override fun setupGithubButtonListener(githubUrl: String, state: String) {
button_github.setOnClickListener {
// TODO
// startActivityForResult(context?.githubWebViewIntent(url, state), REQUEST_CODE_FOR_OAUTH)
// activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
} }
override fun enableLoginByGoogle() { override fun enableLoginByGoogle() {
button_google.isEnabled = true button_google.isClickable = true
} }
override fun enableLoginByLinkedin() { override fun enableLoginByLinkedin() {
button_linkedin.isEnabled = true button_linkedin.isClickable = true
} }
override fun enableLoginByMeteor() { override fun enableLoginByMeteor() {
button_meteor.isEnabled = true button_meteor.isClickable = true
} }
override fun enableLoginByTwitter() { override fun enableLoginByTwitter() {
button_twitter.isEnabled = true button_twitter.isClickable = true
} }
override fun enableLoginByGitlab() { override fun enableLoginByGitlab() {
button_gitlab.isEnabled = true button_gitlab.isClickable = true
}
override fun setupGitlabButtonListener(gitlabUrl: String, state: String) {
button_gitlab.setOnClickListener {
startActivityForResult(context?.gitlabWebViewIntent(gitlabUrl, state), REQUEST_CODE_FOR_OAUTH)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
} }
override fun setupFabListener() { override fun setupFabListener() {
...@@ -253,8 +277,8 @@ class LoginFragment : Fragment(), LoginView { ...@@ -253,8 +277,8 @@ class LoginFragment : Fragment(), LoginView {
social_accounts_container.postDelayed({ social_accounts_container.postDelayed({
(0..social_accounts_container.childCount) (0..social_accounts_container.childCount)
.mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton } .mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton }
.filter { it.isEnabled } .filter { it.isClickable }
.forEach { it.visibility = View.VISIBLE } .forEach { it.setVisible(true)}
}, 1000) }, 1000)
} }
...@@ -284,13 +308,10 @@ class LoginFragment : Fragment(), LoginView { ...@@ -284,13 +308,10 @@ class LoginFragment : Fragment(), LoginView {
} }
private fun showThreeSocialAccountsMethods() { private fun showThreeSocialAccountsMethods() {
var count = 0 (0..social_accounts_container.childCount)
for (i in 0..social_accounts_container.childCount) { .mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton }
val view = social_accounts_container.getChildAt(i) as? ImageButton ?: continue .filter { it.isClickable }
if (view.isEnabled && count < 3) { .take(3)
view.visibility = View.VISIBLE .forEach { it.setVisible(true) }
count++
}
}
} }
} }
\ No newline at end of file
...@@ -2,9 +2,9 @@ package chat.rocket.android.chatroom.adapter ...@@ -2,9 +2,9 @@ package chat.rocket.android.chatroom.adapter
import android.support.annotation.IntDef import android.support.annotation.IntDef
const val PEOPLE = 0L const val PEOPLE = 0
const val ROOMS = 1L const val ROOMS = 1
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
@IntDef(value = [PEOPLE, ROOMS]) @IntDef(PEOPLE, ROOMS)
annotation class AutoCompleteType annotation class AutoCompleteType
...@@ -53,7 +53,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -53,7 +53,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val manager = factory.create(currentServer) private val manager = factory.create(currentServer)
private val client = manager.client private val client = manager.client
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!! private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private val messagesChannel = Channel<Message>() private val messagesChannel = Channel<Message>()
private var chatRoomId: String? = null private var chatRoomId: String? = null
...@@ -402,7 +402,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -402,7 +402,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
} }
} }
fun spotlight(query: String, @AutoCompleteType type: Long, filterSelfOut: Boolean = false) { fun spotlight(query: String, @AutoCompleteType type: Int, filterSelfOut: Boolean = false) {
launchUI(strategy) { launchUI(strategy) {
try { try {
val (users, rooms) = client.spotlight(query) val (users, rooms) = client.spotlight(query)
......
...@@ -24,7 +24,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag ...@@ -24,7 +24,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
getSettingsInteractor: GetSettingsInteractor) { getSettingsInteractor: GetSettingsInteractor) {
private val client = factory.create(serverInteractor.get()!!) private val client = factory.create(serverInteractor.get()!!)
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!! private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private var pinnedMessagesListOffset: Int = 0 private var pinnedMessagesListOffset: Int = 0
/** /**
......
...@@ -37,7 +37,7 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -37,7 +37,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
serverInteractor: GetCurrentServerInteractor, serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor) { getSettingsInteractor: GetSettingsInteractor) {
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!! private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private val baseUrl = settings.baseUrl() private val baseUrl = settings.baseUrl()
private val currentUsername: String? = localRepository.get(LocalRepository.USERNAME_KEY) private val currentUsername: String? = localRepository.get(LocalRepository.USERNAME_KEY)
private val token = tokenRepository.get() private val token = tokenRepository.get()
......
...@@ -36,7 +36,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -36,7 +36,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val client = manager.client private val client = manager.client
private var reloadJob: Deferred<List<ChatRoom>>? = null private var reloadJob: Deferred<List<ChatRoom>>? = null
private val settings = settingsRepository.get(currentServer)!! private val settings = settingsRepository.get(currentServer)
private val subscriptionsChannel = Channel<StreamMessage<BaseRoom>>() private val subscriptionsChannel = Channel<StreamMessage<BaseRoom>>()
private val stateChannel = Channel<State>() private val stateChannel = Channel<State>()
......
...@@ -152,7 +152,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -152,7 +152,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
recycler_view.itemAnimator = DefaultItemAnimator() recycler_view.itemAnimator = DefaultItemAnimator()
// TODO - use a ViewModel Mapper instead of using settings on the adapter // TODO - use a ViewModel Mapper instead of using settings on the adapter
recycler_view.adapter = ChatRoomsAdapter(this, recycler_view.adapter = ChatRoomsAdapter(this,
settingsRepository.get(serverInteractor.get()!!)!!) { chatRoom -> settingsRepository.get(serverInteractor.get()!!)) { chatRoom ->
presenter.loadChatRoom(chatRoom) presenter.loadChatRoom(chatRoom)
} }
} }
......
...@@ -25,6 +25,27 @@ object UrlHelper { ...@@ -25,6 +25,27 @@ object UrlHelper {
fun getCasUrl(casLoginUrl: String, serverUrl: String, token: String): String = fun getCasUrl(casLoginUrl: String, serverUrl: String, token: String): String =
removeTrailingSlash(casLoginUrl) + "?service=" + removeTrailingSlash(serverUrl) + "/_cas/" + token removeTrailingSlash(casLoginUrl) + "?service=" + removeTrailingSlash(serverUrl) + "/_cas/" + token
/**
* Returns the Github Oauth URL.
*
* @param clientId The GitHub client ID.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Github Oauth URL.
*/
// TODO: Fix github url.
fun getGithubOauthUrl(clientId: String, state: String): String =
"https://github.com/login/oauth/authorize?scope=user:email&client_id=$clientId&state=$state"
/**
* Returns the Gitlab Oauth URL.
*
* @param clientId The Gitlab client ID.
* @param serverUrl The server URL.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Gitlab Oauth URL.
*/
fun getGitlabOauthUrl(clientId: String, serverUrl: String, state: String): String =
"https://gitlab.com/oauth/authorize?client_id=$clientId&redirect_uri=${removeTrailingSlash(serverUrl)}/_oauth/gitlab?close&response_type=code&state=$state&scope=read_user"
/** /**
* Returns the server's Terms of Service URL. * Returns the server's Terms of Service URL.
* *
......
...@@ -3,12 +3,15 @@ package chat.rocket.android.util.extensions ...@@ -3,12 +3,15 @@ package chat.rocket.android.util.extensions
import android.text.Spannable import android.text.Spannable
import android.text.Spanned import android.text.Spanned
import android.text.TextUtils import android.text.TextUtils
import android.util.Base64
import android.util.Patterns import android.util.Patterns
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.widget.emoji.EmojiParser import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import org.json.JSONObject
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import java.net.URLDecoder
import java.security.SecureRandom import java.security.SecureRandom
fun String.ifEmpty(value: String): String { fun String.ifEmpty(value: String): String {
...@@ -33,7 +36,23 @@ fun EditText.erase() { ...@@ -33,7 +36,23 @@ fun EditText.erase() {
} }
} }
fun String.isEmailValid(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches() fun String.isEmail(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches()
fun String.encodeToBase64(): String {
return Base64.encodeToString(this.toByteArray(charset("UTF-8")), Base64.NO_WRAP)
}
fun String.decodeFromBase64(): String {
return Base64.decode(this, Base64.DEFAULT).toString(charset("UTF-8"))
}
fun String.decodeUrl(): String {
return URLDecoder.decode(this, "UTF-8")
}
fun String.toJsonObject(): JSONObject {
return JSONObject(this)
}
fun generateRandomString(stringLength: Int): String { fun generateRandomString(stringLength: Int): String {
val base = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" val base = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
......
...@@ -13,7 +13,7 @@ import chat.rocket.android.R ...@@ -13,7 +13,7 @@ import chat.rocket.android.R
import kotlinx.android.synthetic.main.activity_web_view.* import kotlinx.android.synthetic.main.activity_web_view.*
import kotlinx.android.synthetic.main.app_bar.* import kotlinx.android.synthetic.main.app_bar.*
fun Context.webViewIntent(webPageUrl: String, casToken: String): Intent { fun Context.casWebViewIntent(webPageUrl: String, casToken: String): Intent {
return Intent(this, CasWebViewActivity::class.java).apply { return Intent(this, CasWebViewActivity::class.java).apply {
putExtra(INTENT_WEB_PAGE_URL, webPageUrl) putExtra(INTENT_WEB_PAGE_URL, webPageUrl)
putExtra(INTENT_CAS_TOKEN, casToken) putExtra(INTENT_CAS_TOKEN, casToken)
...@@ -21,7 +21,7 @@ fun Context.webViewIntent(webPageUrl: String, casToken: String): Intent { ...@@ -21,7 +21,7 @@ fun Context.webViewIntent(webPageUrl: String, casToken: String): Intent {
} }
private const val INTENT_WEB_PAGE_URL = "web_page_url" private const val INTENT_WEB_PAGE_URL = "web_page_url"
private const val INTENT_CAS_TOKEN = "cas_token" const val INTENT_CAS_TOKEN = "cas_token"
class CasWebViewActivity : AppCompatActivity() { class CasWebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String private lateinit var webPageUrl: String
...@@ -49,14 +49,14 @@ class CasWebViewActivity : AppCompatActivity() { ...@@ -49,14 +49,14 @@ class CasWebViewActivity : AppCompatActivity() {
if (web_view.canGoBack()) { if (web_view.canGoBack()) {
web_view.goBack() web_view.goBack()
} else { } else {
finishActivity(false) closeView()
} }
} }
private fun setupToolbar() { private fun setupToolbar() {
toolbar.title = getString(R.string.title_authentication) toolbar.title = getString(R.string.title_authentication)
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp) toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
toolbar.setNavigationOnClickListener { finishActivity(false) } toolbar.setNavigationOnClickListener { closeView() }
} }
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
...@@ -64,16 +64,16 @@ class CasWebViewActivity : AppCompatActivity() { ...@@ -64,16 +64,16 @@ class CasWebViewActivity : AppCompatActivity() {
web_view.settings.javaScriptEnabled = true web_view.settings.javaScriptEnabled = true
web_view.webViewClient = object : WebViewClient() { web_view.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
// The user can be already logged in the CAS, so check if the URL contains the "ticket" word // The user may have already been logged in the CAS, so check if the URL contains the "ticket" word
// (that means he/she is successful authenticated and we don't need to wait until the page is finished. // (that means the user is successful authenticated and we don't need to wait until the page is fully loaded).
if (url.contains("ticket")) { if (url.contains("ticket")) {
finishActivity(true) closeView(Activity.RESULT_OK)
} }
} }
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
if (url.contains("ticket")) { if (url.contains("ticket")) {
finishActivity(true) closeView(Activity.RESULT_OK)
} else { } else {
view_loading.hide() view_loading.hide()
} }
...@@ -82,13 +82,9 @@ class CasWebViewActivity : AppCompatActivity() { ...@@ -82,13 +82,9 @@ class CasWebViewActivity : AppCompatActivity() {
web_view.loadUrl(webPageUrl) web_view.loadUrl(webPageUrl)
} }
private fun finishActivity(setResultOk: Boolean) { private fun closeView(activityResult: Int = Activity.RESULT_CANCELED) {
if (setResultOk) { setResult(activityResult, Intent().putExtra(INTENT_CAS_TOKEN, casToken))
setResult(Activity.RESULT_OK, Intent().putExtra(INTENT_CAS_TOKEN, casToken))
finish() finish()
} else {
super.onBackPressed()
}
overridePendingTransition(R.anim.hold, R.anim.slide_down) overridePendingTransition(R.anim.hold, R.anim.slide_down)
} }
} }
\ No newline at end of file
package chat.rocket.android.webview.gitlab.ui
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.net.toUri
import chat.rocket.android.R
import chat.rocket.android.util.extensions.decodeUrl
import chat.rocket.android.util.extensions.toJsonObject
import kotlinx.android.synthetic.main.activity_web_view.*
import kotlinx.android.synthetic.main.app_bar.*
import org.json.JSONObject
fun Context.gitlabWebViewIntent(webPageUrl: String, state: String): Intent {
return Intent(this, GitlabWebViewActivity::class.java).apply {
putExtra(INTENT_WEB_PAGE_URL, webPageUrl)
putExtra(INTENT_STATE, state)
}
}
private const val INTENT_WEB_PAGE_URL = "web_page_url"
private const val INTENT_STATE = "state"
private const val JSON_CREDENTIAL_TOKEN = "credentialToken"
private const val JSON_CREDENTIAL_SECRET = "credentialSecret"
const val INTENT_OAUTH_CREDENTIAL_TOKEN = "credential_token"
const val INTENT_OAUTH_CREDENTIAL_SECRET = "credential_secret"
// Shows a WebView to the user authenticate with your Gitlab credentials.
class GitlabWebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String
private lateinit var state: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view)
webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL)
requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" }
state = intent.getStringExtra(INTENT_STATE)
requireNotNull(state) { "no state provided in Intent extras" }
setupToolbar()
}
override fun onResume() {
super.onResume()
setupWebView()
}
override fun onBackPressed() {
if (web_view.canGoBack()) {
web_view.goBack()
} else {
closeView()
}
}
private fun setupToolbar() {
toolbar.title = getString(R.string.title_authentication)
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
toolbar.setNavigationOnClickListener { closeView() }
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
web_view.settings.javaScriptEnabled = true
web_view.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
if (url.contains(JSON_CREDENTIAL_TOKEN) && url.contains(JSON_CREDENTIAL_SECRET)) {
if (isStateValid(url)) {
val jsonResult = url.decodeUrl()
.substringAfter("#")
.toJsonObject()
val credentialToken = getCredentialToken(jsonResult)
val credentialSecret = getCredentialSecret(jsonResult)
if (credentialToken.isNotEmpty() && credentialSecret.isNotEmpty()) {
closeView(Activity.RESULT_OK, credentialToken, credentialSecret)
}
}
}
view_loading.hide()
}
}
web_view.loadUrl(webPageUrl)
}
// If the states matches, then try to get the code, otherwise the request was created by a third party and the process should be aborted.
private fun isStateValid(url: String): Boolean =
url.substringBefore("#").toUri().getQueryParameter(INTENT_STATE) == state
private fun getCredentialToken(json: JSONObject): String =
json.optString(JSON_CREDENTIAL_TOKEN)
private fun getCredentialSecret(json: JSONObject): String =
json.optString(JSON_CREDENTIAL_SECRET)
private fun closeView(activityResult: Int = Activity.RESULT_CANCELED, credentialToken: String? = null, credentialSecret: String? = null) {
setResult(activityResult, Intent().putExtra(INTENT_OAUTH_CREDENTIAL_TOKEN, credentialToken).putExtra(INTENT_OAUTH_CREDENTIAL_SECRET, credentialSecret))
finish()
overridePendingTransition(R.anim.hold, R.anim.slide_down)
}
}
\ No newline at end of file
...@@ -113,6 +113,7 @@ ...@@ -113,6 +113,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_facebook" android:contentDescription="@string/msg_content_description_log_in_using_facebook"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_facebook" android:src="@drawable/ic_facebook"
...@@ -124,6 +125,7 @@ ...@@ -124,6 +125,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_github" android:contentDescription="@string/msg_content_description_log_in_using_github"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_github" android:src="@drawable/ic_github"
...@@ -135,6 +137,7 @@ ...@@ -135,6 +137,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_google" android:contentDescription="@string/msg_content_description_log_in_using_google"
android:foreground="?android:attr/selectableItemBackground" android:foreground="?android:attr/selectableItemBackground"
android:src="@drawable/ic_google" android:src="@drawable/ic_google"
...@@ -146,6 +149,7 @@ ...@@ -146,6 +149,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_linkedin" android:contentDescription="@string/msg_content_description_log_in_using_linkedin"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_linkedin" android:src="@drawable/ic_linkedin"
...@@ -157,6 +161,7 @@ ...@@ -157,6 +161,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_meteor" android:contentDescription="@string/msg_content_description_log_in_using_meteor"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_meteor" android:src="@drawable/ic_meteor"
...@@ -168,6 +173,7 @@ ...@@ -168,6 +173,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_twitter" android:contentDescription="@string/msg_content_description_log_in_using_twitter"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_twitter" android:src="@drawable/ic_twitter"
...@@ -179,6 +185,7 @@ ...@@ -179,6 +185,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_gitlab" android:contentDescription="@string/msg_content_description_log_in_using_gitlab"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_gitlab" android:src="@drawable/ic_gitlab"
......
...@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME ...@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment