mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-04-14 19:56:32 +00:00
Attempt workaround for apple sign in
This commit is contained in:
parent
a8914d4b95
commit
a5f5e98aad
10 changed files with 303 additions and 8 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue