Skip to content

Commit 5d74347

Browse files
authored
Merge pull request #91 from simple-login/feature/connect-with-proton
Connect with Proton
2 parents 54678cd + 0adb2f8 commit 5d74347

15 files changed

Lines changed: 381 additions & 15 deletions

File tree

SimpleLogin/app/src/main/AndroidManifest.xml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,21 @@
5252
<activity
5353
android:name=".module.home.HomeActivity"
5454
android:theme="@style/HomeActivityTheme"
55-
android:windowSoftInputMode="adjustResize" />
55+
android:windowSoftInputMode="adjustResize"
56+
android:exported="true"
57+
android:launchMode="singleTask">
58+
<intent-filter>
59+
<action android:name="android.intent.action.VIEW"/>
60+
<category android:name="android.intent.category.DEFAULT"/>
61+
<category android:name="android.intent.category.BROWSABLE"/>
62+
63+
<data
64+
android:scheme="@string/simplelogin_scheme"
65+
android:host="*"
66+
android:path="/link"
67+
/>
68+
</intent-filter>
69+
</activity>
5670
<activity android:name=".module.startup.LocalAuthActivity" />
5771

5872
<!--
@@ -67,7 +81,11 @@
6781
<category android:name="android.intent.category.DEFAULT"/>
6882
<category android:name="android.intent.category.BROWSABLE"/>
6983

70-
<data android:scheme="auth.simplelogin"/>
84+
<data
85+
android:scheme="@string/simplelogin_scheme"
86+
android:host="*"
87+
android:path="/login"
88+
/>
7189
</intent-filter>
7290
</activity>
7391
<activity android:name=".module.login.VerificationActivity" />

SimpleLogin/app/src/main/java/io/simplelogin/android/module/home/HomeActivity.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import com.google.android.material.navigation.NavigationView
2222
import io.simplelogin.android.R
2323
import io.simplelogin.android.databinding.ActivityHomeBinding
2424
import io.simplelogin.android.module.about.AboutFragment
25+
import io.simplelogin.android.module.settings.SettingsFragment
2526
import io.simplelogin.android.module.settings.view.AvatarView
2627
import io.simplelogin.android.module.startup.StartupActivity
28+
import io.simplelogin.android.utils.SLApiService
2729
import io.simplelogin.android.utils.SLSharedPreferences
2830
import io.simplelogin.android.utils.baseclass.BaseAppCompatActivity
2931
import io.simplelogin.android.utils.extension.getVersionName
@@ -69,6 +71,19 @@ class HomeActivity : BaseAppCompatActivity(), NavigationView.OnNavigationItemSel
6971
viewModel.setUserInfo(userInfo)
7072
}
7173

74+
override fun onNewIntent(intent: Intent?) {
75+
super.onNewIntent(intent)
76+
// In case we received a new intent is because the Connect with Proton worked
77+
// Refresh the user info
78+
if (supportFragmentManager.fragments.size == 0) return
79+
val navHostFragment = supportFragmentManager.fragments[0] as? NavHostFragment ?: return
80+
val settingsFragment = navHostFragment.childFragmentManager.fragments.find { it is SettingsFragment }
81+
if (settingsFragment != null) {
82+
val casted = settingsFragment as SettingsFragment
83+
casted.onNewIntent(intent)
84+
}
85+
}
86+
7287
override fun onBackPressed() {
7388
if (binding.mainDrawer.isDrawerOpen(binding.navigationView)) {
7489
// When navigationView is already open and user press back

SimpleLogin/app/src/main/java/io/simplelogin/android/module/home/HomeViewModel.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import android.app.Application
44
import androidx.lifecycle.AndroidViewModel
55
import androidx.lifecycle.LiveData
66
import androidx.lifecycle.MutableLiveData
7+
import io.simplelogin.android.utils.SLApiService
8+
import io.simplelogin.android.utils.enums.SLError
79
import io.simplelogin.android.utils.model.UserInfo
810

911
class HomeViewModel(application: Application) : AndroidViewModel(application) {

SimpleLogin/app/src/main/java/io/simplelogin/android/module/login/LoginActivity.kt

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@ import android.text.Editable
1010
import android.text.TextWatcher
1111
import android.view.KeyEvent
1212
import android.view.View
13-
import androidx.browser.customtabs.CustomTabColorSchemeParams
14-
import androidx.browser.customtabs.CustomTabsIntent
1513
import com.google.android.material.bottomsheet.BottomSheetBehavior
1614
import com.google.android.material.dialog.MaterialAlertDialogBuilder
1715
import io.simplelogin.android.R
1816
import io.simplelogin.android.databinding.ActivityLoginBinding
1917
import io.simplelogin.android.module.home.HomeActivity
18+
import io.simplelogin.android.utils.LoginWithProtonUtils
2019
import io.simplelogin.android.utils.SLApiService
2120
import io.simplelogin.android.utils.SLSharedPreferences
2221
import io.simplelogin.android.utils.baseclass.BaseAppCompatActivity
@@ -413,16 +412,7 @@ class LoginActivity : BaseAppCompatActivity() {
413412

414413
private fun loginWithProton() {
415414
dismissKeyboard()
416-
val baseUrl = SLSharedPreferences.getApiUrl(this)
417-
val url = "${baseUrl}/auth/proton/login?mode=apikey"
418-
419-
val builder = CustomTabsIntent.Builder()
420-
.setDefaultColorSchemeParams(CustomTabColorSchemeParams.Builder()
421-
.setToolbarColor(R.color.protonMain)
422-
.build())
423-
424-
val customTabsIntent = builder.build()
425-
customTabsIntent.launchUrl(this, Uri.parse(url))
415+
LoginWithProtonUtils.launchLoginWithProton(this)
426416
}
427417

428418
private fun setLoading(loading: Boolean) {

SimpleLogin/app/src/main/java/io/simplelogin/android/module/settings/SettingsFragment.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import android.view.LayoutInflater
1717
import android.view.View
1818
import android.view.View.*
1919
import android.view.ViewGroup
20+
import android.widget.Toast
2021
import androidx.appcompat.app.AppCompatDelegate
2122
import androidx.fragment.app.activityViewModels
2223
import androidx.navigation.fragment.findNavController
@@ -26,8 +27,10 @@ import io.simplelogin.android.databinding.DialogViewEditTextBinding
2627
import io.simplelogin.android.databinding.FragmentSettingsBinding
2728
import io.simplelogin.android.module.home.HomeActivity
2829
import io.simplelogin.android.module.home.HomeViewModel
30+
import io.simplelogin.android.utils.LoginWithProtonUtils
2931
import io.simplelogin.android.utils.SLSharedPreferences
3032
import io.simplelogin.android.utils.baseclass.BaseFragment
33+
import io.simplelogin.android.utils.extension.runOnUiThread
3134
import io.simplelogin.android.utils.extension.toastError
3235
import io.simplelogin.android.utils.extension.toastShortly
3336
import io.simplelogin.android.utils.model.UserInfo
@@ -88,6 +91,15 @@ class SettingsFragment : BaseFragment(), HomeActivity.OnBackPressed {
8891
binding.contactsAccessView.updateSwitchState()
8992
}
9093

94+
fun onNewIntent(intent: Intent?) {
95+
// Update the user info
96+
viewModel.refreshUserInfo {
97+
runOnUiThread {
98+
Toast.makeText(requireContext(), R.string.your_proton_account_has_been_linked, Toast.LENGTH_SHORT).show()
99+
}
100+
}
101+
}
102+
91103
private fun bind(userInfo: UserInfo) {
92104
binding.profileInfoCardView.visibility = VISIBLE
93105
binding.profileInfoCardView.bind(userInfo)
@@ -96,6 +108,22 @@ class SettingsFragment : BaseFragment(), HomeActivity.OnBackPressed {
96108
SettingsFragmentDirections.actionSettingsFragmentToPremiumFragment()
97109
)
98110
}
111+
112+
// Connected with Proton
113+
binding.connectWithProtonCardView.bind(userInfo.connectedProtonAddress)
114+
binding.connectWithProtonCardView.setOnConnectButtonClickListener {
115+
viewModel.getTemporaryToken { token ->
116+
context?.let { context ->
117+
LoginWithProtonUtils.launchLinkWithProton(context, token)
118+
}
119+
}
120+
}
121+
122+
binding.connectWithProtonCardView.setOnUnlinkButtonClickListener {
123+
alertUnlinkProtonAccount {
124+
viewModel.unlinkProtonAccount()
125+
}
126+
}
99127
}
100128

101129
private fun bind(userSettings: UserSettings) {
@@ -203,6 +231,12 @@ class SettingsFragment : BaseFragment(), HomeActivity.OnBackPressed {
203231
viewModel.onHandleUserSettingsUpdatedComplete()
204232
}
205233
}
234+
235+
viewModel.eventProtonAccountUnlinked.observe(viewLifecycleOwner) { updated ->
236+
if (updated) {
237+
Toast.makeText(requireContext(), R.string.your_proton_account_has_been_unlinked, Toast.LENGTH_SHORT).show()
238+
}
239+
}
206240
}
207241

208242
private fun alertModificationOptions() {
@@ -233,6 +267,15 @@ class SettingsFragment : BaseFragment(), HomeActivity.OnBackPressed {
233267
.show()
234268
}
235269

270+
private fun alertUnlinkProtonAccount(onConfirm: () -> Unit) {
271+
MaterialAlertDialogBuilder(requireContext(), R.style.SlAlertDialogTheme)
272+
.setTitle(R.string.proton_unlink_account_confirmation_title)
273+
.setMessage(R.string.proton_unlink_account_confirmation_content)
274+
.setNegativeButton(R.string.dialog_cancel_button, null)
275+
.setPositiveButton(R.string.dialog_confirm_button) { _, _ -> onConfirm()}
276+
.show()
277+
}
278+
236279
private fun askForPhotoLibraryPermission() {
237280
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
238281
val readPermission = Manifest.permission.READ_EXTERNAL_STORAGE

SimpleLogin/app/src/main/java/io/simplelogin/android/module/settings/SettingsViewModel.kt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.simplelogin.android.utils.SLApiService
88
import io.simplelogin.android.utils.baseclass.BaseViewModel
99
import io.simplelogin.android.utils.enums.SLError
1010
import io.simplelogin.android.utils.model.DomainLite
11+
import io.simplelogin.android.utils.model.TemporaryToken
1112
import io.simplelogin.android.utils.model.UserInfo
1213
import io.simplelogin.android.utils.model.UserSettings
1314

@@ -116,6 +117,26 @@ class SettingsViewModel(val context: Context) : BaseViewModel(context) {
116117
}
117118
}
118119

120+
fun refreshUserInfo(
121+
ignoreIsFetching: Boolean = false,
122+
onCompletion: () -> Unit = {}
123+
) {
124+
if (!ignoreIsFetching) {
125+
if (_isFetching.value == true) return
126+
_isFetching.postValue(true)
127+
}
128+
129+
SLApiService.fetchUserInfo(apiKey) { result ->
130+
_isFetching.postValue(false)
131+
result.onSuccess {
132+
userInfo = it
133+
onCompletion()
134+
}
135+
result.onFailure { _error.postValue(it as SLError) }
136+
_eventUserInfoUpdated.postValue(true)
137+
}
138+
}
139+
119140
// Update
120141
fun updateUserSettings(option: UserSettings.Option) {
121142
if (_isFetching.value == true) return
@@ -128,4 +149,38 @@ class SettingsViewModel(val context: Context) : BaseViewModel(context) {
128149
_evenUserSettingsUpdated.postValue(true)
129150
}
130151
}
152+
153+
// Unlink Proton account
154+
private val _eventProtonAccountUnlinked = MutableLiveData<Boolean>()
155+
val eventProtonAccountUnlinked: LiveData<Boolean>
156+
get() = _eventProtonAccountUnlinked
157+
158+
fun unlinkProtonAccount() {
159+
if (_isFetching.value == true) return
160+
_isFetching.postValue(true)
161+
162+
SLApiService.unlinkProtonAccount(apiKey) { result ->
163+
// Refresh the user info but do not take into account the isFetching value,
164+
// as we don't want the loading dialog to flash
165+
result.onSuccess {
166+
refreshUserInfo(ignoreIsFetching = true) {
167+
_eventProtonAccountUnlinked.postValue(true)
168+
}
169+
}
170+
result.onFailure {
171+
_error.postValue(it as SLError)
172+
_isFetching.postValue(false)
173+
}
174+
}
175+
}
176+
177+
fun getTemporaryToken(completion: (TemporaryToken) -> Unit) {
178+
if (_isFetching.value == true) return
179+
_isFetching.postValue(true)
180+
SLApiService.getTemporaryToken(apiKey) { result ->
181+
result.onSuccess { completion(it) }
182+
result.onFailure { _error.postValue(it as SLError) }
183+
_isFetching.postValue(false)
184+
}
185+
}
131186
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.simplelogin.android.module.settings.view
2+
3+
import android.content.Context
4+
import android.util.AttributeSet
5+
import android.view.LayoutInflater
6+
import android.view.View
7+
import android.widget.RelativeLayout
8+
import androidx.core.content.ContextCompat
9+
import io.simplelogin.android.R
10+
import io.simplelogin.android.databinding.LayoutConnectWithProtonViewBinding
11+
12+
class ConnectWithProtonView: RelativeLayout {
13+
// Initializer
14+
constructor(context: Context) : super(context)
15+
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
16+
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
17+
18+
private val binding =
19+
LayoutConnectWithProtonViewBinding.inflate(LayoutInflater.from(context), this, true)
20+
21+
init {
22+
background = ContextCompat.getDrawable(context, android.R.color.transparent)
23+
}
24+
25+
fun bind(connectedProtonAddress: String?) {
26+
if (connectedProtonAddress != null) {
27+
binding.connectWithProtonButton.visibility = View.GONE
28+
binding.unlinkProtonAccountButton.visibility = View.VISIBLE
29+
binding.connectWithProtonInfoText.visibility = View.GONE
30+
binding.accountConnectedWithProtonText.visibility = View.VISIBLE
31+
binding.accountConnectedWithProtonText.text = context.getString(R.string.currently_linked_proton_account, connectedProtonAddress)
32+
} else {
33+
binding.connectWithProtonButton.visibility = View.VISIBLE
34+
binding.unlinkProtonAccountButton.visibility = View.GONE
35+
binding.connectWithProtonInfoText.visibility = View.VISIBLE
36+
binding.accountConnectedWithProtonText.visibility = View.GONE
37+
}
38+
}
39+
40+
fun setOnConnectButtonClickListener(listener: () -> Unit) {
41+
binding.connectWithProtonButton.setOnClickListener {
42+
listener()
43+
}
44+
}
45+
46+
fun setOnUnlinkButtonClickListener(listener: () -> Unit) {
47+
binding.unlinkProtonAccountButton.setOnClickListener {
48+
listener()
49+
}
50+
}
51+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.simplelogin.android.utils
2+
3+
import android.content.Context
4+
import android.net.Uri
5+
import androidx.browser.customtabs.CustomTabColorSchemeParams
6+
import androidx.browser.customtabs.CustomTabsIntent
7+
import io.simplelogin.android.R
8+
import io.simplelogin.android.utils.model.TemporaryToken
9+
10+
11+
object LoginWithProtonUtils {
12+
fun launchLoginWithProton(context: Context) {
13+
val baseUrl = SLSharedPreferences.getApiUrl(context)
14+
val scheme = context.getString(R.string.simplelogin_scheme)
15+
val next = "/login"
16+
val url = "${baseUrl}/auth/proton/login?mode=apikey&action=login&scheme=${scheme}&next=${next}"
17+
launchChromeTab(context, url)
18+
}
19+
20+
fun launchLinkWithProton(context: Context, temporaryToken: TemporaryToken) {
21+
val baseUrl = SLSharedPreferences.getApiUrl(context)
22+
val scheme = context.getString(R.string.simplelogin_scheme)
23+
val action = "link"
24+
val next = "/link"
25+
val nextQuery = "/auth/proton/login?action=${action}&next=${next}&scheme=${scheme}"
26+
val nextQueryEncoded = Uri.encode(nextQuery)
27+
val url = "${baseUrl}/auth/api_to_cookie?token=${temporaryToken.token}&next=${nextQueryEncoded}"
28+
launchChromeTab(context, url)
29+
}
30+
31+
private fun launchChromeTab(context: Context, url: String) {
32+
val builder = CustomTabsIntent.Builder()
33+
.setDefaultColorSchemeParams(CustomTabColorSchemeParams.Builder()
34+
.setToolbarColor(R.color.protonMain)
35+
.build())
36+
37+
val customTabsIntent = builder.build()
38+
customTabsIntent.launchUrl(context, Uri.parse(url))
39+
}
40+
}

0 commit comments

Comments
 (0)