Attempt workaround for apple sign in

This commit is contained in:
Phillip Thelen 2020-01-21 14:54:17 +01:00
parent a8914d4b95
commit a5f5e98aad
10 changed files with 303 additions and 8 deletions

View file

@ -17,7 +17,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'io.fabric.tools:gradle:1.31.0'
classpath 'io.fabric.tools:gradle:1.+'
classpath('com.noveogroup.android:check:1.2.5') {
exclude module: 'checkstyle'
exclude module: 'pmd-java'
@ -115,10 +115,10 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.2'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2'
//Push Notifications
implementation 'com.google.firebase:firebase-core:17.2.1'
implementation 'com.google.firebase:firebase-core:17.2.2'
implementation 'com.google.firebase:firebase-messaging:20.1.0'
implementation 'com.google.firebase:firebase-config:19.1.0'
implementation 'com.google.firebase:firebase-perf:19.0.4'
implementation 'com.google.firebase:firebase-perf:19.0.5'
implementation 'com.google.android.gms:play-services-auth:17.0.0'
implementation 'io.realm:android-adapters:3.1.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@ -135,6 +135,8 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
implementation 'com.willowtreeapps:signinwithapplebutton:0.2'
implementation project(':shared')
}
@ -152,7 +154,7 @@ android {
resConfigs "en", "bg", "de", "en-rGB", "es", "fr", "hr-rHR", "in", "it", "iw", "ja", "ko", "lt", "nl", "pl", "pt-rBR", "pt-rPT", "ru", "tr", "zh", "zh-rTW"
versionCode 2346
versionName "2.4.2"
versionName "2.5"
}
viewBinding {

View file

@ -213,6 +213,15 @@
android:drawableLeft="@drawable/google_icon"
style="@style/LoginButton"/>
<Button
android:id="@+id/apple_login_button"
android:layout_width="match_parent"
android:layout_marginTop="@dimen/spacing_large"
android:layout_height="@dimen/diamond_button_height"
android:text="@string/login_btn_apple"
android:drawableLeft="@drawable/google_icon"
style="@style/LoginButton"/>
<Button
android:id="@+id/forgot_password"
android:layout_width="match_parent"

View file

@ -461,6 +461,7 @@
<string name="not_owned">Not owned</string>
<string name="login_btn_fb">Login with Facebook</string>
<string name="login_btn_google">Login with Google</string>
<string name="login_btn_apple">Sign in with Apple</string>
<string name="register_btn_fb">Sign up with Facebook</string>
<string name="register_btn_google">Sign up with Google</string>
<string name="action_back">Back</string>

View file

@ -138,6 +138,8 @@ interface ApiService {
@POST("user/auth/social")
fun connectSocial(@Body auth: UserAuthSocial): Flowable<HabitResponse<UserAuthResponse>>
@POST("user/auth/apple")
fun loginApple(@Body auth: Map<String, Any>): Flowable<HabitResponse<UserAuthResponse>>
@POST("user/sleep")
fun sleep(): Flowable<HabitResponse<Boolean>>

View file

@ -103,6 +103,8 @@ interface ApiClient {
fun connectUser(username: String, password: String): Flowable<UserAuthResponse>
fun connectSocial(network: String, userId: String, accessToken: String): Flowable<UserAuthResponse>
fun loginApple(authToken: String): Flowable<UserAuthResponse>
fun sleep(): Flowable<Boolean>
fun revive(): Flowable<User>

View file

@ -174,6 +174,10 @@ class ApiClientImpl//private OnHabitsAPIResult mResultListener;
return this.apiService.connectSocial(auth).compose(configureApiCallObserver())
}
override fun loginApple(authToken: String): Flowable<UserAuthResponse> {
return apiService.loginApple(mapOf(Pair("code", authToken))).compose(configureApiCallObserver())
}
override fun accept(throwable: Throwable) {
val throwableClass = throwable.javaClass
if (SocketTimeoutException::class.java.isAssignableFrom(throwableClass)) {

View file

@ -0,0 +1,64 @@
package com.habitrpg.android.habitica.helpers
import android.net.Uri
import android.os.Build
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.annotation.RequiresApi
class SignInWebViewClient(
private val attempt: SignInWithAppleService.AuthenticationAttempt,
private val callback: (SignInWithAppleResult) -> Unit
) : WebViewClient() {
// for API levels < 24
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
return isUrlOverridden(view, Uri.parse(url))
}
@RequiresApi(Build.VERSION_CODES.N)
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
return isUrlOverridden(view, request?.url)
}
private fun isUrlOverridden(view: WebView?, url: Uri?): Boolean {
return when {
url == null -> {
false
}
url.toString().contains("appleid.apple.com") -> {
view?.loadUrl(url.toString())
true
}
(url.toString().contains(attempt.redirectUri) || url.toString().contains("userID")) -> {
Log.d("Apple Sign in", "Web view was forwarded to redirect URI")
val userID = url.getQueryParameter("userID")
val apiKey = url.getQueryParameter("key")
when {
userID == null || apiKey == null -> {
callback(SignInWithAppleResult.Failure(IllegalArgumentException("data not returned")))
}
else -> {
callback(SignInWithAppleResult.Success(userID, apiKey, url.getQueryParameter("newUser") == "true"))
}
}
true
}
else -> {
false
}
}
}
}
sealed class SignInWithAppleResult {
data class Success(val userID: String, val apiKey: String, val newUser: Boolean) : SignInWithAppleResult()
data class Failure(val error: Throwable) : SignInWithAppleResult()
object Cancel : SignInWithAppleResult()
}

View file

@ -0,0 +1,103 @@
package com.habitrpg.android.habitica.helpers
import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebView
import androidx.fragment.app.DialogFragment
import com.habitrpg.android.habitica.R
class SignInWebViewDialogFragment : DialogFragment() {
companion object {
private const val AUTHENTICATION_ATTEMPT_KEY = "authenticationAttempt"
private const val WEB_VIEW_KEY = "webView"
fun newInstance(authenticationAttempt: SignInWithAppleService.AuthenticationAttempt): SignInWebViewDialogFragment {
val fragment = SignInWebViewDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(AUTHENTICATION_ATTEMPT_KEY, authenticationAttempt)
}
return fragment
}
}
private lateinit var authenticationAttempt: SignInWithAppleService.AuthenticationAttempt
private var callback: ((SignInWithAppleResult) -> Unit)? = null
private val webViewIfCreated: WebView?
get() = view as? WebView
fun configure(callback: (SignInWithAppleResult) -> Unit) {
this.callback = callback
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
authenticationAttempt = arguments?.getParcelable(AUTHENTICATION_ATTEMPT_KEY)!!
setStyle(STYLE_NORMAL, R.style.sign_in_with_apple_button_DialogTheme)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
val webView = WebView(context).apply {
settings.apply {
javaScriptEnabled = true
javaScriptCanOpenWindowsAutomatically = true
}
}
webView.webViewClient = SignInWebViewClient(authenticationAttempt, ::onCallback)
if (savedInstanceState != null) {
savedInstanceState.getBundle(WEB_VIEW_KEY)?.run {
webView.restoreState(this)
}
} else {
webView.loadUrl(authenticationAttempt.authenticationUri)
}
return webView
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBundle(
WEB_VIEW_KEY,
Bundle().apply {
webViewIfCreated?.saveState(this)
}
)
}
override fun onStart() {
super.onStart()
dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
onCallback(SignInWithAppleResult.Cancel)
}
// SignInWithAppleCallback
private fun onCallback(result: SignInWithAppleResult) {
dialog?.dismiss()
val callback = callback
if (callback == null) {
Log.e("Apple Sign in", "Callback is not configured")
return
}
callback(result)
}
}

View file

@ -0,0 +1,89 @@
package com.habitrpg.android.habitica.helpers
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import androidx.fragment.app.FragmentManager
import com.willowtreeapps.signinwithapplebutton.SignInWithAppleConfiguration
import java.util.*
class SignInWithAppleService(
private val fragmentManager: FragmentManager,
private val fragmentTag: String,
private val configuration: SignInWithAppleConfiguration,
private val callback: (SignInWithAppleResult) -> Unit
) {
init {
val fragmentIfShown =
fragmentManager.findFragmentByTag(fragmentTag) as? SignInWebViewDialogFragment
fragmentIfShown?.configure(callback)
}
data class AuthenticationAttempt(
val authenticationUri: String,
val redirectUri: String,
val state: String
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "invalid",
parcel.readString() ?: "invalid",
parcel.readString() ?: "invalid"
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(authenticationUri)
parcel.writeString(redirectUri)
parcel.writeString(state)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<AuthenticationAttempt> {
override fun createFromParcel(parcel: Parcel) = AuthenticationAttempt(parcel)
override fun newArray(size: Int): Array<AuthenticationAttempt?> = arrayOfNulls(size)
/*
The authentication page URI we're creating is based off the URI constructed by Apple's JavaScript SDK,
which is why certain fields (like the version, v) are included in the URI construction.
We have to build this URI ourselves because Apple's behavior in JavaScript is to POST the response,
while we need a GET so we can retrieve the authentication code and verify the state
merely by intercepting the URL.
See the Sign In With Apple Javascript SDK for comparison:
https://developer.apple.com/documentation/signinwithapplejs/configuring_your_webpage_for_sign_in_with_apple
*/
fun create(
configuration: SignInWithAppleConfiguration,
state: String = UUID.randomUUID().toString()
): AuthenticationAttempt {
val authenticationUri = Uri
.parse("https://appleid.apple.com/auth/authorize")
.buildUpon().apply {
appendQueryParameter("response_type", "code")
appendQueryParameter("v", "1.1.6")
appendQueryParameter("client_id", configuration.clientId)
appendQueryParameter("redirect_uri", configuration.redirectUri)
appendQueryParameter("scope", configuration.scope)
appendQueryParameter("state", state)
appendQueryParameter("response_mode", "form_post")
}
.build()
.toString()
return AuthenticationAttempt(authenticationUri, configuration.redirectUri, state)
}
}
}
fun show() {
val fragment = SignInWebViewDialogFragment.newInstance(AuthenticationAttempt.create(configuration))
fragment.configure(callback)
fragment.show(fragmentManager, fragmentTag)
}
}

View file

@ -31,6 +31,7 @@ import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.GooglePlayServicesUtil
import com.google.android.gms.common.Scopes
import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.api.HostConfig
@ -40,10 +41,7 @@ import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.extensions.addCancelButton
import com.habitrpg.android.habitica.extensions.addCloseButton
import com.habitrpg.android.habitica.extensions.addOkButton
import com.habitrpg.android.habitica.helpers.AmplitudeManager
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.KeyHelper
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.*
import com.habitrpg.android.habitica.models.auth.UserAuthResponse
import com.habitrpg.android.habitica.proxy.CrashlyticsProxy
import com.habitrpg.android.habitica.ui.helpers.bindView
@ -51,6 +49,7 @@ import com.habitrpg.android.habitica.ui.helpers.dismissKeyboard
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.login.LockableScrollView
import com.habitrpg.android.habitica.ui.views.login.LoginBackgroundView
import com.willowtreeapps.signinwithapplebutton.SignInWithAppleConfiguration
import io.reactivex.Flowable
import io.reactivex.exceptions.Exceptions
import io.reactivex.functions.Consumer
@ -96,6 +95,7 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
private val forgotPasswordButton: Button by bindView(R.id.forgot_password)
private val facebookLoginButton: Button by bindView(R.id.fb_login_button)
private val googleLoginButton: Button by bindView(R.id.google_login_button)
private val appleLoginButton: Button by bindView(R.id.apple_login_button)
private var callbackManager = CallbackManager.Factory.create()
private var googleEmail: String? = null
@ -177,6 +177,25 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
forgotPasswordButton.setOnClickListener { onForgotPasswordClicked() }
facebookLoginButton.setOnClickListener { handleFacebookLogin() }
googleLoginButton.setOnClickListener { handleGoogleLogin() }
appleLoginButton.setOnClickListener {
val configuration = SignInWithAppleConfiguration(
clientId = BuildConfig.APPLE_AUTH_CLIENT_ID,
redirectUri = "https://habitrpg-delta.herokuapp.com/api/v4/user/auth/apple",
scope = "name email"
)
val fragmentTag = "SignInWithAppleButton-SignInWebViewDialogFragment"
SignInWithAppleService(supportFragmentManager, fragmentTag, configuration) { result ->
when (result) {
is SignInWithAppleResult.Success -> {
val response = UserAuthResponse()
response.id = result.userID
response.apiToken = result.apiKey
response.newUser = result.newUser
}
}
}.show()
}
}
private fun setupFacebookLogin() {