Bimba.git

ref: new-login

app/src/main/java/xyz/apiote/bimba/czwek/onboarding/OnboardingActivity.kt


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.onboarding

import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.core.widget.addTextChangedListener
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationResponse
import net.openid.appauth.AuthorizationService
import net.openid.appauth.AuthorizationServiceConfiguration
import net.openid.appauth.AuthorizationServiceConfiguration.RetrieveConfigurationCallback
import net.openid.appauth.ResponseTypeValues
import xyz.apiote.bimba.czwek.api.Server
import xyz.apiote.bimba.czwek.databinding.ActivityOnboardingBinding
import xyz.apiote.bimba.czwek.settings.feeds.FeedChooserActivity


class OnboardingActivity : AppCompatActivity() {
	private var _binding: ActivityOnboardingBinding? = null
	private val binding get() = _binding!!
	private lateinit var preferences: SharedPreferences
	private lateinit var authState: AuthState

	private val activityLauncher =
		registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
			if (!FirstRunActivity.getFirstRun(this)) {
				finish()
			}
		}

	override fun onCreate(savedInstanceState: Bundle?) {
		enableEdgeToEdge()
		super.onCreate(savedInstanceState)
		_binding = ActivityOnboardingBinding.inflate(layoutInflater)
		setContentView(binding.root)

		ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets ->
			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
			v.updatePadding(right = insets.right, left = insets.left, bottom = insets.bottom)
			windowInsets
		}
		preferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE)

		preferences.edit(true) {
			putBoolean(IN_FEEDS_TRANSACTION, true)
		}

		binding.server.editText!!.addTextChangedListener { editable ->
			binding.accountButton.isEnabled = !editable.isNullOrBlank()
			binding.anonymousButton.isEnabled = !editable.isNullOrBlank()
		}

		if (!FirstRunActivity.getFirstRun(this)) {
			Server.get(this).let { server ->
				binding.server.editText!!.setText(server.host)
			}
		}

		binding.accountButton.setOnClickListener {
			// TODO OIDC flow https://github.com/openid/AppAuth-Android
			oidcFlow()
		}

		binding.anonymousButton.setOnClickListener {
			moveOn()
		}
	}

	private fun setServer(hostname: String) {
		preferences.edit(true) {
			putString(Server.HOST_KEY, hostname)
		}
	}

	private fun moveOn() {
		// TODO test 401/403
		// TODO show rate-limit info, terms and privacy
		binding.server.editText?.text?.toString()?.let { serverAddress -> setServer(serverAddress) }
		activityLauncher.launch(Intent(this, FeedChooserActivity::class.java))
	}

	private fun oidcFlow() {
		AuthorizationServiceConfiguration.fetchFromIssuer(
			Uri.parse("https://oauth-bimba.apiote.xyz"),
			RetrieveConfigurationCallback { serviceConfiguration, ex ->
				if (ex != null) {
					Log.e("OIDC", "failed to fetch configuration")
					return@RetrieveConfigurationCallback
				}
				authState = AuthState(serviceConfiguration!!)

				/*
				{"client_id":"46e1be27-a747-4507-a1d4-c383f137745f","client_name":"Bimba","client_secret_expires_at":0,"client_uri":"https://bimba.app","contacts":["questions@bimba.app"],"created_at":"2024-09-09T12:41:30Z","grant_types":["authorization_code","refresh_token"],"jwks":{},"logo_uri":"https://bimba.app/bimba.svg","metadata":{},"owner":"","policy_uri":"","redirect_uris":["bimba://callback"],"registration_access_token":"ory_at_xQWn3kMRuev5ZmCM_BgwnuOo57GRKWz-7LAA8EH2OTA.GXZCCf7XCg8fIwjpGZjZGdgJPMSsMSM9E-6B7bE8nB0","registration_client_uri":"https://oauth-bimba.apiote.xyz/oauth2/register/","request_object_signing_alg":"RS256","response_types":["code","id_token"],"scope":"openid offline","skip_consent":false,"skip_logout_consent":false,"subject_type":"public","token_endpoint_auth_method":"none","tos_uri":"","updated_at":"2024-09-09T14:41:29.982386+02:00","userinfo_signed_response_alg":"none"}
				 */
				val authRequest = AuthorizationRequest.Builder(
					serviceConfiguration,
					"46e1be27-a747-4507-a1d4-c383f137745f",
					ResponseTypeValues.CODE,
					Uri.parse("bimba://callback")
				)
					.setScope("openid offline")
					.build();

				val authIntent = AuthorizationService(this).getAuthorizationRequestIntent(authRequest)
				startActivityForResult(authIntent, RC_AUTH)
			})
	}

	override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
		super.onActivityResult(requestCode, resultCode, data)
		if (requestCode == RC_AUTH) {
			val resp = AuthorizationResponse.fromIntent(data!!)
			val ex = AuthorizationException.fromIntent(data)
			if (resp != null) {
				authState.update(resp, ex)
				AuthorizationService(this).performTokenRequest(
					resp.createTokenExchangeRequest()
				) { tokenResponse, tokenException ->
					if (tokenResponse != null) {
						authState.update(resp, ex)
						getSharedPreferences("auth", MODE_PRIVATE).edit {
							putString("state", authState.jsonSerializeString())
						}
						if (tokenResponse.idToken == null) {
							// TODO openid is needed; show some dialogue
						}
						Log.i("OIDC", "got token: id: ${tokenResponse.idToken}\n access: ${tokenResponse.accessToken}\n params: ${tokenResponse.additionalParameters}")
						// TODO moveOn()
					} else {
						// FIXME token exchange failed: Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method).
						Log.e("OIDC", "token exchange failed: ${tokenException!!.message}")
						tokenException.printStackTrace()
					}
				}
			} else {
				Log.e("OIDC", "auth failed: ${ex!!.message}")
			}
		}
	}

	companion object {
		const val PREFERENCES_NAME = "shp"
		const val IN_FEEDS_TRANSACTION = "inFeedsTransaction"
		const val RC_AUTH = 42
	}
}