mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-05-18 03:39:00 +00:00
Improve social login handling for logged in users
This commit is contained in:
parent
bb01dc9b7d
commit
3b6251f114
23 changed files with 840 additions and 625 deletions
|
|
@ -34,7 +34,8 @@
|
|||
android:label="@string/app_name"
|
||||
android:theme="@style/LaunchAppTheme"
|
||||
android:windowSoftInputMode="stateHidden|adjustResize"
|
||||
android:configChanges="orientation|screenSize">
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true">
|
||||
<nav-graph android:value="@navigation/navigation" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
|
@ -54,7 +55,8 @@
|
|||
android:name=".ui.activities.PrefsActivity"
|
||||
android:parentActivityName=".ui.activities.MainActivity"
|
||||
android:label="@string/PS_settings_title"
|
||||
tools:ignore="UnusedAttribute">
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:exported="true">
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
|
@ -160,7 +162,8 @@
|
|||
android:name=".ui.activities.FullProfileActivity"
|
||||
android:parentActivityName=".ui.activities.MainActivity"
|
||||
android:theme="@style/MainAppTheme"
|
||||
tools:ignore="UnusedAttribute">
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".ui.activities.MainActivity" />
|
||||
|
|
@ -209,18 +212,21 @@
|
|||
<action android:name="REJECT_QUEST_INVITE"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".receivers.TaskAlarmBootReceiver" android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
|
||||
<receiver android:name=".receivers.TaskAlarmBootReceiver" android:permission="android.permission.RECEIVE_BOOT_COMPLETED"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity android:name=".ui.activities.AddTaskWidgetActivity">
|
||||
<activity android:name=".ui.activities.AddTaskWidgetActivity"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ui.activities.HabitButtonWidgetActivity">
|
||||
<activity android:name=".ui.activities.HabitButtonWidgetActivity"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||
</intent-filter>
|
||||
|
|
@ -248,7 +254,8 @@
|
|||
</provider>
|
||||
|
||||
<receiver android:name=".widget.AvatarStatsWidgetProvider"
|
||||
android:label="@string/stats_widget_label">
|
||||
android:label="@string/stats_widget_label"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
|
@ -256,7 +263,8 @@
|
|||
android:resource="@xml/avatar_widget_info" />
|
||||
</receiver>
|
||||
<receiver android:name=".widget.DailiesWidgetProvider"
|
||||
android:label="@string/widget_dailies">
|
||||
android:label="@string/widget_dailies"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
|
@ -264,7 +272,8 @@
|
|||
android:resource="@xml/daily_list_widget_info" />
|
||||
</receiver>
|
||||
<receiver android:name=".widget.TodoListWidgetProvider"
|
||||
android:label="@string/widget_todo_list">
|
||||
android:label="@string/widget_todo_list"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
|
@ -272,7 +281,8 @@
|
|||
android:resource="@xml/todo_list_widget_info" />
|
||||
</receiver>
|
||||
<receiver android:name=".widget.HabitButtonWidgetProvider"
|
||||
android:label="@string/widget_habit_button">
|
||||
android:label="@string/widget_habit_button"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ buildscript {
|
|||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.2'
|
||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||
classpath 'net.sourceforge.pmd:pmd-java:5.5.3'
|
||||
}
|
||||
}
|
||||
|
|
@ -98,8 +98,6 @@ dependencies {
|
|||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation "io.mockk:mockk-android:1.12.0"
|
||||
debugImplementation 'androidx.test:monitor:1.4.0'
|
||||
debugImplementation "androidx.fragment:fragment-testing:1.3.6"
|
||||
|
||||
|
||||
//Leak Detection
|
||||
|
|
@ -112,13 +110,13 @@ dependencies {
|
|||
implementation 'com.google.firebase:firebase-config'
|
||||
implementation 'com.google.firebase:firebase-perf'
|
||||
implementation 'com.google.android.gms:play-services-auth:19.2.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.30"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31"
|
||||
implementation 'com.nex3z:flow-layout:1.2.2'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1"
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.4.0"
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.6"
|
||||
|
|
@ -139,7 +137,7 @@ dependencies {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion '30.0.2'
|
||||
testOptions {
|
||||
unitTests {
|
||||
|
|
@ -155,8 +153,10 @@ android {
|
|||
buildConfigField "String", "TESTING_LEVEL", "\"production\""
|
||||
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 3077
|
||||
versionName "3.4.1"
|
||||
versionCode 3093
|
||||
versionName "3.4.1.1"
|
||||
|
||||
targetSdkVersion 31
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
|
|
|||
56
Habitica/res/layout/preference_button.xml
Normal file
56
Habitica/res/layout/preference_button.xml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Layout for a visually child-like Preference in a PreferenceActivity. -->
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:baselineAligned="false"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:paddingEnd="16dip"
|
||||
android:paddingStart="16dip"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dip"
|
||||
android:layout_marginStart="16dip"
|
||||
android:layout_marginEnd="6dip"
|
||||
android:layout_marginTop="6dip"
|
||||
android:layout_weight="1" >
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="@color/text_primary"
|
||||
tools:text="Title"/>
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/summary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignStart="@android:id/title"
|
||||
android:layout_below="@android:id/title"
|
||||
android:maxLines="4"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
</RelativeLayout>
|
||||
<TextView android:id="@+id/extra_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="0dp"
|
||||
android:layout_gravity="center"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:background="@color/transparent"
|
||||
|
||||
android:paddingEnd="0dp"/>
|
||||
</LinearLayout>
|
||||
|
|
@ -1184,4 +1184,16 @@
|
|||
<string name="damage_pending">%.01f dmg pending</string>
|
||||
<string name="x_remaining">%s remaining</string>
|
||||
<string name="sale_ends_in">Sale ends in %s</string>
|
||||
<string name="my_account">My Account</string>
|
||||
<string name="public_profile">Public Profile</string>
|
||||
<string name="about_me">About Me</string>
|
||||
<string name="api">API</string>
|
||||
<string name="account_info">Account Info</string>
|
||||
<string name="login_methods">Login Methods</string>
|
||||
<string name="apple">Apple</string>
|
||||
<string name="not_connected">Not connected</string>
|
||||
<string name="add_password">Add password</string>
|
||||
<string name="connect">Connect</string>
|
||||
<string name="disconnect">Disconnect</string>
|
||||
<string name="add">Add</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:title="@string/PS_contact_title">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_account_header"
|
||||
|
|
@ -10,111 +9,114 @@
|
|||
android:layout="@layout/preference_category">
|
||||
|
||||
<PreferenceScreen
|
||||
android:key="profile"
|
||||
android:key="my_account"
|
||||
android:layout="@layout/preference_child_summary"
|
||||
android:summary="@string/profile_summary"
|
||||
android:title="@string/profile">
|
||||
<EditTextPreference
|
||||
android:key="display_name"
|
||||
android:layout="@layout/preference_child_summary"
|
||||
android:negativeButtonText="@string/cancel"
|
||||
android:positiveButtonText="@string/save"
|
||||
android:title="@string/display_name" />
|
||||
<EditTextPreference
|
||||
android:key="photo_url"
|
||||
android:layout="@layout/preference_child_summary"
|
||||
android:negativeButtonText="@string/cancel"
|
||||
android:positiveButtonText="@string/save"
|
||||
android:title="@string/photo_url" />
|
||||
<EditTextPreference
|
||||
android:key="about"
|
||||
android:layout="@layout/preference_child_summary"
|
||||
android:negativeButtonText="@string/cancel"
|
||||
android:positiveButtonText="@string/save"
|
||||
android:title="@string/about" />
|
||||
</PreferenceScreen>
|
||||
android:title="@string/my_account">
|
||||
<PreferenceCategory
|
||||
android:title="@string/account_info"
|
||||
android:key="account_info"
|
||||
android:layout="@layout/preference_category">
|
||||
<Preference
|
||||
android:key="username"
|
||||
android:title="@string/username"
|
||||
android:selectable="true"
|
||||
android:persistent="false"
|
||||
android:shouldDisableView="false"
|
||||
android:layout="@layout/preference_child_summary" />
|
||||
<Preference android:title="@string/confirm_username"
|
||||
android:key="confirm_username"
|
||||
app:isPreferenceVisible="false"
|
||||
android:layout="@layout/preference_child_summary" />
|
||||
<Preference
|
||||
android:key="email"
|
||||
android:title="@string/email"
|
||||
android:selectable="true"
|
||||
android:persistent="false"
|
||||
android:shouldDisableView="false"
|
||||
android:layout="@layout/preference_child_summary"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceScreen
|
||||
android:key="authentication"
|
||||
android:title="@string/authentication"
|
||||
android:summary="@string/authentication_summary"
|
||||
app:key="authentication"
|
||||
android:layout="@layout/preference_child_summary">
|
||||
<Preference
|
||||
android:key="login_name"
|
||||
android:title="@string/login_name"
|
||||
android:selectable="true"
|
||||
android:persistent="false"
|
||||
android:shouldDisableView="false"
|
||||
android:layout="@layout/preference_child_summary" />
|
||||
<Preference android:title="@string/confirm_username"
|
||||
android:key="confirm_username"
|
||||
android:layout="@layout/preference_child_summary" />
|
||||
<PreferenceCategory
|
||||
android:title="@string/login_methods"
|
||||
android:key="account_info"
|
||||
android:layout="@layout/preference_category">
|
||||
<com.habitrpg.android.habitica.ui.views.ExtraLabelPreference
|
||||
android:title="@string/password"
|
||||
android:key="password"
|
||||
android:layout="@layout/preference_button"
|
||||
/>
|
||||
<com.habitrpg.android.habitica.ui.views.ExtraLabelPreference
|
||||
android:title="@string/google"
|
||||
android:key="google_auth"
|
||||
android:layout="@layout/preference_button"
|
||||
/>
|
||||
<com.habitrpg.android.habitica.ui.views.ExtraLabelPreference
|
||||
android:title="@string/facebook"
|
||||
android:key="facebook_auth"
|
||||
/>
|
||||
<com.habitrpg.android.habitica.ui.views.ExtraLabelPreference
|
||||
android:title="@string/apple"
|
||||
android:key="apple_auth"
|
||||
/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:title="@string/public_profile"
|
||||
android:key="public_profile"
|
||||
android:layout="@layout/preference_category">
|
||||
<EditTextPreference
|
||||
android:key="display_name"
|
||||
android:layout="@layout/preference_child_summary"
|
||||
android:negativeButtonText="@string/cancel"
|
||||
android:positiveButtonText="@string/save"
|
||||
android:title="@string/display_name" />
|
||||
<EditTextPreference
|
||||
android:key="about"
|
||||
android:layout="@layout/preference_child_summary"
|
||||
android:negativeButtonText="@string/cancel"
|
||||
android:positiveButtonText="@string/save"
|
||||
android:title="@string/about_me" />
|
||||
<EditTextPreference
|
||||
android:key="photo_url"
|
||||
android:layout="@layout/preference_child_summary"
|
||||
android:negativeButtonText="@string/cancel"
|
||||
android:positiveButtonText="@string/save"
|
||||
android:title="@string/photo_url" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:title="@string/api"
|
||||
android:key="api"
|
||||
android:layout="@layout/preference_category">
|
||||
<Preference
|
||||
android:key="user_id"
|
||||
android:title="@string/SP_userID_title"
|
||||
android:selectable="true"
|
||||
android:persistent="false"
|
||||
android:shouldDisableView="false"
|
||||
android:summary="@string/SP_userID_summary"
|
||||
android:layout="@layout/preference_child_summary"/>
|
||||
|
||||
<Preference android:title="@string/change_password"
|
||||
android:key="change_password"
|
||||
android:layout="@layout/preference_child_summary" />
|
||||
<Preference
|
||||
android:key="email"
|
||||
android:title="@string/email"
|
||||
android:selectable="true"
|
||||
android:persistent="false"
|
||||
android:shouldDisableView="false"
|
||||
android:layout="@layout/preference_child_summary"/>
|
||||
<Preference
|
||||
android:key="add_local_auth"
|
||||
android:title="@string/add_local_authentication"
|
||||
android:selectable="true"
|
||||
android:persistent="false"
|
||||
android:shouldDisableView="false"
|
||||
android:layout="@layout/preference_child_summary"/>
|
||||
<Preference
|
||||
android:key="authentication_methods"
|
||||
android:title="@string/authentication_methods"
|
||||
android:selectable="false"
|
||||
android:persistent="false"
|
||||
android:shouldDisableView="false"
|
||||
android:layout="@layout/preference_child_summary"/>
|
||||
<Preference
|
||||
android:key="subscription_status"
|
||||
android:title="@string/subscription_status"
|
||||
android:selectable="true"
|
||||
android:persistent="false"
|
||||
android:shouldDisableView="false"
|
||||
android:layout="@layout/preference_child_summary"/>
|
||||
<Preference
|
||||
android:key="api_token"
|
||||
android:title="@string/SP_APIToken_title"
|
||||
android:selectable="true"
|
||||
android:persistent="false"
|
||||
android:shouldDisableView="false"
|
||||
android:summary="@string/SP_APIToken_summary"
|
||||
android:layout="@layout/preference_child_summary"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/danger_zone"
|
||||
android:layout="@layout/preference_category">
|
||||
<Preference android:title="@string/reset_account"
|
||||
android:persistent="false"
|
||||
android:key="reset_account"
|
||||
android:layout="@layout/preference_child_summary" />
|
||||
<Preference android:title="@string/delete_account"
|
||||
android:persistent="false"
|
||||
android:key="delete_account"
|
||||
android:layout="@layout/preference_child_summary" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
|
||||
<PreferenceScreen android:title="API"
|
||||
android:key="api"
|
||||
android:layout="@layout/preference_child_summary">
|
||||
<Preference
|
||||
android:key="@string/SP_userID"
|
||||
android:title="@string/SP_userID_title"
|
||||
android:selectable="true"
|
||||
android:persistent="false"
|
||||
android:shouldDisableView="false"
|
||||
android:summary="@string/SP_userID_summary"
|
||||
android:layout="@layout/preference_child_summary"/>
|
||||
|
||||
<Preference
|
||||
android:key="@string/SP_APIToken"
|
||||
android:title="@string/SP_APIToken_title"
|
||||
android:selectable="true"
|
||||
android:persistent="false"
|
||||
android:shouldDisableView="false"
|
||||
android:summary="@string/SP_APIToken_summary"
|
||||
android:layout="@layout/preference_child_summary"/>
|
||||
</PreferenceScreen>
|
||||
|
||||
<Preference
|
||||
android:key="choose_class"
|
||||
tools:title="Change Class"
|
||||
|
|
|
|||
|
|
@ -139,6 +139,9 @@ interface ApiService {
|
|||
@POST("user/auth/social")
|
||||
fun connectSocial(@Body auth: UserAuthSocial): Flowable<HabitResponse<UserAuthResponse>>
|
||||
|
||||
@DELETE("user/auth/social/{network}")
|
||||
fun disconnectSocial(@Path("network") network: String): Flowable<HabitResponse<Void>>
|
||||
|
||||
@POST("user/auth/apple")
|
||||
fun loginApple(@Body auth: Map<String, Any>): Flowable<HabitResponse<UserAuthResponse>>
|
||||
|
||||
|
|
|
|||
|
|
@ -60,11 +60,9 @@ import com.habitrpg.android.habitica.ui.fragments.inventory.stable.MountDetailRe
|
|||
import com.habitrpg.android.habitica.ui.fragments.inventory.stable.PetDetailRecyclerFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.inventory.stable.StableFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.inventory.stable.StableRecyclerFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.preferences.APIPreferenceFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.preferences.AuthenticationPreferenceFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.preferences.AccountPreferenceFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.preferences.EmailNotificationsPreferencesFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.preferences.PreferencesFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.preferences.ProfilePreferencesFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.preferences.PushNotificationsPreferencesFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.purchases.GemsPurchaseFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.purchases.GiftBalanceGemsFragment;
|
||||
|
|
@ -100,6 +98,7 @@ import com.habitrpg.android.habitica.ui.fragments.support.SupportMainFragment;
|
|||
import com.habitrpg.android.habitica.ui.fragments.tasks.TaskRecyclerViewFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.tasks.TasksFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.tasks.TeamBoardFragment;
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.AuthenticationViewModel;
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.GroupViewModel;
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.InboxViewModel;
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel;
|
||||
|
|
@ -277,12 +276,6 @@ public interface UserComponent {
|
|||
|
||||
void inject(FixCharacterValuesActivity fixCharacterValuesActivity);
|
||||
|
||||
void inject(AuthenticationPreferenceFragment authenticationPreferenceFragment);
|
||||
|
||||
void inject(ProfilePreferencesFragment profilePreferencesFragment);
|
||||
|
||||
void inject(APIPreferenceFragment apiPreferenceFragment);
|
||||
|
||||
void inject(StatsFragment statsFragment);
|
||||
|
||||
void inject(BulkAllocateStatsDialog bulkAllocateStatsDialog);
|
||||
|
|
@ -352,4 +345,7 @@ public interface UserComponent {
|
|||
void inject(@NotNull EquipmentOverviewViewModel equipmentOverviewViewModel);
|
||||
|
||||
void inject(@NotNull AvatarStatsWidgetFactory avatarStatsWidgetFactory);
|
||||
void inject(@NotNull AccountPreferenceFragment accountPreferenceFragment);
|
||||
|
||||
void inject(@NotNull AuthenticationViewModel authenticationViewModel);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,8 @@ interface ApiClient {
|
|||
fun connectUser(username: String, password: String): Flowable<UserAuthResponse>
|
||||
|
||||
fun connectSocial(network: String, userId: String, accessToken: String): Flowable<UserAuthResponse>
|
||||
fun disconnectSocial(network: String): Flowable<Void>
|
||||
|
||||
fun loginApple(authToken: String): Flowable<UserAuthResponse>
|
||||
|
||||
fun sleep(): Flowable<Boolean>
|
||||
|
|
|
|||
|
|
@ -176,6 +176,10 @@ class ApiClientImpl // private OnHabitsAPIResult mResultListener;
|
|||
return this.apiService.connectSocial(auth).compose(configureApiCallObserver())
|
||||
}
|
||||
|
||||
override fun disconnectSocial(network: String): Flowable<Void> {
|
||||
return this.apiService.disconnectSocial(network).compose(configureApiCallObserver())
|
||||
}
|
||||
|
||||
override fun loginApple(authToken: String): Flowable<UserAuthResponse> {
|
||||
return apiService.loginApple(mapOf(Pair("code", authToken))).compose(configureApiCallObserver())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,38 @@ package com.habitrpg.android.habitica.models.user
|
|||
import com.google.gson.annotations.SerializedName
|
||||
import com.habitrpg.android.habitica.models.BaseObject
|
||||
import com.habitrpg.android.habitica.models.auth.LocalAuthentication
|
||||
import com.habitrpg.android.habitica.models.user.auth.SocialAuthentication
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.RealmClass
|
||||
|
||||
@RealmClass(embedded = true)
|
||||
open class Authentication : RealmObject(), BaseObject {
|
||||
fun findFirstSocialEmail(): String? {
|
||||
for (auth in listOf(googleAuthentication, appleAuthentication, facebookAuthentication)) {
|
||||
if (auth?.emails?.isNotEmpty() == true) {
|
||||
return auth.emails.first()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val hasPassword: Boolean
|
||||
get() = localAuthentication?.email != null
|
||||
@SerializedName("local")
|
||||
var localAuthentication: LocalAuthentication? = null
|
||||
@SerializedName("google")
|
||||
var googleAuthentication: SocialAuthentication? = null
|
||||
@SerializedName("apple")
|
||||
var appleAuthentication: SocialAuthentication? = null
|
||||
@SerializedName("facebook")
|
||||
var facebookAuthentication: SocialAuthentication? = null
|
||||
|
||||
var hasFacebookAuth = false
|
||||
var hasGoogleAuth = false
|
||||
var hasAppleAuth = false
|
||||
val hasGoogleAuth: Boolean
|
||||
get() = googleAuthentication?.emails?.isEmpty() == false
|
||||
val hasAppleAuth: Boolean
|
||||
get() = appleAuthentication?.emails?.isEmpty() == false
|
||||
val hasFacebookAuth: Boolean
|
||||
get() = facebookAuthentication?.emails?.isEmpty() == false
|
||||
|
||||
var timestamps: AuthenticationTimestamps? = null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.habitrpg.android.habitica.models.user.auth
|
||||
|
||||
import com.habitrpg.android.habitica.models.BaseObject
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.RealmClass
|
||||
|
||||
@RealmClass(embedded = true)
|
||||
open class SocialAuthentication : RealmObject(), BaseObject {
|
||||
var emails: RealmList<String> = RealmList()
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import com.habitrpg.android.habitica.ui.views.subscriptions.SubscriptionOptionVi
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.solovyev.android.checkout.Inventory
|
||||
import org.solovyev.android.checkout.Sku
|
||||
|
|
@ -109,8 +110,10 @@ class GiftSubscriptionActivity : BaseActivity() {
|
|||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val subscriptions = purchaseHandler?.getAllGiftSubscriptionProducts()
|
||||
skus = subscriptions?.skus ?: return@launch
|
||||
for (sku in skus) {
|
||||
updateButtonLabel(sku, sku.price, subscriptions)
|
||||
withContext(Dispatchers.Main) {
|
||||
for (sku in skus) {
|
||||
updateButtonLabel(sku, sku.price, subscriptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.habitrpg.android.habitica.ui.activities
|
|||
import android.accounts.AccountManager
|
||||
import android.animation.*
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
|
|
@ -21,48 +20,27 @@ import android.widget.EditText
|
|||
import android.widget.LinearLayout
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.facebook.AccessToken
|
||||
import com.facebook.CallbackManager
|
||||
import com.facebook.FacebookCallback
|
||||
import com.facebook.FacebookException
|
||||
import com.facebook.FacebookSdk
|
||||
import com.facebook.login.LoginManager
|
||||
import com.facebook.login.LoginResult
|
||||
import com.google.android.gms.auth.GoogleAuthException
|
||||
import com.google.android.gms.auth.GoogleAuthUtil
|
||||
import com.google.android.gms.auth.GooglePlayServicesAvailabilityException
|
||||
import com.google.android.gms.auth.UserRecoverableAuthException
|
||||
import com.google.android.gms.common.*
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
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
|
||||
import com.habitrpg.android.habitica.components.UserComponent
|
||||
import com.habitrpg.android.habitica.data.ApiClient
|
||||
import com.habitrpg.android.habitica.data.UserRepository
|
||||
import com.habitrpg.android.habitica.databinding.ActivityLoginBinding
|
||||
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.extensions.updateStatusBarColor
|
||||
import com.habitrpg.android.habitica.helpers.*
|
||||
import com.habitrpg.android.habitica.models.auth.UserAuthResponse
|
||||
import com.habitrpg.android.habitica.proxy.AnalyticsManager
|
||||
import com.habitrpg.android.habitica.ui.helpers.dismissKeyboard
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.AuthenticationViewModel
|
||||
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
|
||||
import com.willowtreeapps.signinwithapplebutton.SignInWithAppleConfiguration
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.exceptions.Exceptions
|
||||
import io.reactivex.rxjava3.functions.Consumer
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
|
||||
class LoginActivity : BaseActivity() {
|
||||
|
||||
private lateinit var viewModel: AuthenticationViewModel
|
||||
private lateinit var binding: ActivityLoginBinding
|
||||
|
||||
@Inject
|
||||
|
|
@ -70,23 +48,13 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
|
|||
@Inject
|
||||
lateinit var sharedPrefs: SharedPreferences
|
||||
@Inject
|
||||
lateinit var hostConfig: HostConfig
|
||||
@Inject
|
||||
internal lateinit var userRepository: UserRepository
|
||||
@Inject
|
||||
@JvmField
|
||||
var keyHelper: KeyHelper? = null
|
||||
@Inject
|
||||
lateinit var analyticsManager: AnalyticsManager
|
||||
@Inject
|
||||
lateinit var configManager: AppConfigManager
|
||||
|
||||
private var isRegistering: Boolean = false
|
||||
private var isShowingForm: Boolean = false
|
||||
|
||||
private var callbackManager = CallbackManager.Factory.create()
|
||||
private var googleEmail: String? = null
|
||||
private var loginManager = LoginManager.getInstance()
|
||||
|
||||
private val loginClick = View.OnClickListener {
|
||||
binding.PBAsyncTask.visibility = View.VISIBLE
|
||||
|
|
@ -105,7 +73,7 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
|
|||
}
|
||||
apiClient.registerUser(username, email, password, confirmPassword)
|
||||
.subscribe(
|
||||
this@LoginActivity,
|
||||
{ handleAuthResponse(it) },
|
||||
{
|
||||
hideProgress()
|
||||
RxErrorHandler.reportError(it)
|
||||
|
|
@ -119,7 +87,7 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
|
|||
return@OnClickListener
|
||||
}
|
||||
apiClient.connectUser(username, password).subscribe(
|
||||
this@LoginActivity,
|
||||
{ handleAuthResponse(it) },
|
||||
{
|
||||
hideProgress()
|
||||
RxErrorHandler.reportError(it)
|
||||
|
|
@ -140,11 +108,14 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel = AuthenticationViewModel()
|
||||
supportActionBar?.hide()
|
||||
// Set default values to avoid null-responses when requesting unedited settings
|
||||
PreferenceManager.setDefaultValues(this, R.xml.preferences_fragment, false)
|
||||
|
||||
setupFacebookLogin()
|
||||
viewModel.setupFacebookLogin {
|
||||
handleAuthResponse(it)
|
||||
}
|
||||
|
||||
binding.loginBtn.setOnClickListener(loginClick)
|
||||
|
||||
|
|
@ -168,26 +139,12 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
|
|||
binding.showLoginButton.setOnClickListener { showLoginButtonClicked() }
|
||||
binding.backButton.setOnClickListener { backButtonClicked() }
|
||||
binding.forgotPassword.setOnClickListener { onForgotPasswordClicked() }
|
||||
binding.fbLoginButton.setOnClickListener { handleFacebookLogin() }
|
||||
binding.googleLoginButton.setOnClickListener { handleGoogleLogin() }
|
||||
binding.fbLoginButton.setOnClickListener { viewModel.handleFacebookLogin(this) }
|
||||
binding.googleLoginButton.setOnClickListener { viewModel.handleGoogleLogin(this, pickAccountResult) }
|
||||
binding.appleLoginButton.setOnClickListener {
|
||||
val configuration = SignInWithAppleConfiguration(
|
||||
clientId = BuildConfig.APPLE_AUTH_CLIENT_ID,
|
||||
redirectUri = "${hostConfig.address}/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()
|
||||
viewModel.connectApple(supportFragmentManager) {
|
||||
handleAuthResponse(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -196,28 +153,6 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
|
|||
window.updateStatusBarColor(R.color.black_20_alpha, false)
|
||||
}
|
||||
|
||||
private fun setupFacebookLogin() {
|
||||
callbackManager = CallbackManager.Factory.create()
|
||||
loginManager.registerCallback(
|
||||
callbackManager,
|
||||
object : FacebookCallback<LoginResult> {
|
||||
override fun onSuccess(loginResult: LoginResult) {
|
||||
val accessToken = AccessToken.getCurrentAccessToken()
|
||||
compositeSubscription.add(
|
||||
apiClient.connectSocial("facebook", accessToken?.userId ?: "", accessToken?.token ?: "")
|
||||
.subscribe(this@LoginActivity, RxErrorHandler.handleEmptyError())
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCancel() { /* no-on */ }
|
||||
|
||||
override fun onError(exception: FacebookException) {
|
||||
RxErrorHandler.reportError(exception)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (isShowingForm) {
|
||||
hideForm()
|
||||
|
|
@ -293,19 +228,34 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
|
|||
this.resetLayout()
|
||||
}
|
||||
|
||||
private fun handleAuthResponse(response: UserAuthResponse) {
|
||||
hideProgress()
|
||||
dismissKeyboard()
|
||||
viewModel.handleAuthResponse(response)
|
||||
|
||||
if (isRegistering) {
|
||||
FirebaseAnalytics.getInstance(this).logEvent("user_registered", null)
|
||||
}
|
||||
compositeSubscription.add(
|
||||
userRepository.retrieveUser(withTasks = true, forced = true)
|
||||
.subscribe(
|
||||
{
|
||||
if (response.newUser) {
|
||||
this.startSetupActivity()
|
||||
} else {
|
||||
this.startMainActivity()
|
||||
AmplitudeManager.sendEvent("login", AmplitudeManager.EVENT_CATEGORY_BEHAVIOUR, AmplitudeManager.EVENT_HITTYPE_EVENT)
|
||||
}
|
||||
},
|
||||
RxErrorHandler.handleEmptyError()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
callbackManager.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (requestCode == FacebookSdk.getCallbackRequestCodeOffset()) {
|
||||
// This is necessary because the regular login callback is not called for some reason
|
||||
val accessToken = AccessToken.getCurrentAccessToken()
|
||||
if (accessToken != null && accessToken.token != null) {
|
||||
compositeSubscription.add(
|
||||
apiClient.connectSocial("facebook", accessToken.userId, accessToken.token)
|
||||
.subscribe(this@LoginActivity, { hideProgress() })
|
||||
)
|
||||
}
|
||||
viewModel.onActivityResult(requestCode, resultCode, data) {
|
||||
handleAuthResponse(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,27 +266,6 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
|
|||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private fun saveTokens(api: String, user: String) {
|
||||
this.apiClient.updateAuthenticationCredentials(user, api)
|
||||
sharedPrefs.edit {
|
||||
putString(getString(R.string.SP_userID), user)
|
||||
val encryptedKey = if (keyHelper != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
keyHelper?.encrypt(api)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
} else null
|
||||
if (encryptedKey?.length ?: 0 > 5) {
|
||||
putString(user, encryptedKey)
|
||||
} else {
|
||||
// Something might have gone wrong with encryption, so fall back to this.
|
||||
putString(getString(R.string.SP_APIToken), api)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideProgress() {
|
||||
runOnUiThread {
|
||||
binding.PBAsyncTask.visibility = View.GONE
|
||||
|
|
@ -356,141 +285,22 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
|
|||
alert.show()
|
||||
}
|
||||
|
||||
override fun accept(userAuthResponse: UserAuthResponse) {
|
||||
hideProgress()
|
||||
dismissKeyboard()
|
||||
try {
|
||||
saveTokens(userAuthResponse.apiToken, userAuthResponse.id)
|
||||
} catch (e: Exception) {
|
||||
analyticsManager.logException(e)
|
||||
}
|
||||
|
||||
HabiticaBaseApplication.reloadUserComponent()
|
||||
|
||||
if (isRegistering) {
|
||||
FirebaseAnalytics.getInstance(this).logEvent("user_registered", null)
|
||||
}
|
||||
|
||||
compositeSubscription.add(
|
||||
userRepository.retrieveUser(withTasks = true, forced = true)
|
||||
.subscribe(
|
||||
{
|
||||
if (userAuthResponse.newUser) {
|
||||
this.startSetupActivity()
|
||||
} else {
|
||||
this.startMainActivity()
|
||||
AmplitudeManager.sendEvent("login", AmplitudeManager.EVENT_CATEGORY_BEHAVIOUR, AmplitudeManager.EVENT_HITTYPE_EVENT)
|
||||
}
|
||||
},
|
||||
RxErrorHandler.handleEmptyError()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleFacebookLogin() {
|
||||
loginManager.logInWithReadPermissions(this, listOf("user_friends"))
|
||||
}
|
||||
|
||||
private fun handleGoogleLogin() {
|
||||
if (!checkPlayServices()) {
|
||||
return
|
||||
}
|
||||
val accountTypes = arrayOf("com.google")
|
||||
val intent = AccountManager.newChooseAccountIntent(
|
||||
null, null,
|
||||
accountTypes, true, null, null, null, null
|
||||
)
|
||||
try {
|
||||
pickAccountResult.launch(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
val alert = HabiticaAlertDialog(this)
|
||||
alert.setTitle(R.string.authentication_error_title)
|
||||
alert.setMessage(R.string.google_services_missing)
|
||||
alert.addCloseButton()
|
||||
alert.show()
|
||||
}
|
||||
}
|
||||
|
||||
private val pickAccountResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
googleEmail = it?.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
|
||||
handleGoogleLoginResult()
|
||||
viewModel.googleEmail = it?.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
|
||||
viewModel.handleGoogleLoginResult(this, recoverFromPlayServicesErrorResult) {
|
||||
handleAuthResponse(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGoogleLoginResult() {
|
||||
val scopesString = Scopes.PROFILE + " " + Scopes.EMAIL
|
||||
val scopes = "oauth2:$scopesString"
|
||||
compositeSubscription.add(
|
||||
Flowable.defer {
|
||||
try {
|
||||
@Suppress("Deprecation")
|
||||
return@defer Flowable.just(GoogleAuthUtil.getToken(this, googleEmail, scopes))
|
||||
} catch (e: IOException) {
|
||||
throw Exceptions.propagate(e)
|
||||
} catch (e: GoogleAuthException) {
|
||||
throw Exceptions.propagate(e)
|
||||
} catch (e: UserRecoverableException) {
|
||||
return@defer Flowable.empty()
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap { token -> apiClient.connectSocial("google", googleEmail ?: "", token) }
|
||||
.subscribe(
|
||||
this@LoginActivity,
|
||||
{ throwable ->
|
||||
hideProgress()
|
||||
throwable.cause?.let {
|
||||
if (GoogleAuthException::class.java.isAssignableFrom(it.javaClass)) {
|
||||
handleGoogleAuthException(throwable.cause as GoogleAuthException)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleGoogleAuthException(e: Exception) {
|
||||
if (e is GooglePlayServicesAvailabilityException) {
|
||||
// The Google Play services APK is old, disabled, or not present.
|
||||
// Show a dialog created by Google Play services that allows
|
||||
// the user to update the APK
|
||||
val statusCode = e
|
||||
.connectionStatusCode
|
||||
GoogleApiAvailability.getInstance()
|
||||
@Suppress("DEPRECATION")
|
||||
GooglePlayServicesUtil.showErrorDialogFragment(
|
||||
statusCode,
|
||||
this@LoginActivity,
|
||||
REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR
|
||||
) {
|
||||
}
|
||||
} else if (e is UserRecoverableAuthException) {
|
||||
// Unable to authenticate, such as when the user has not yet granted
|
||||
// the app access to the account, but the user can fix this.
|
||||
// Forward the user to an activity in Google Play services.
|
||||
val intent = e.intent
|
||||
recoverFromPlayServicesErrorResult.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private val recoverFromPlayServicesErrorResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
private val recoverFromPlayServicesErrorResult = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode != Activity.RESULT_CANCELED) {
|
||||
handleGoogleLoginResult()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPlayServices(): Boolean {
|
||||
val googleAPI = GoogleApiAvailability.getInstance()
|
||||
val result = googleAPI.isGooglePlayServicesAvailable(this)
|
||||
if (result != ConnectionResult.SUCCESS) {
|
||||
if (googleAPI.isUserResolvableError(result)) {
|
||||
googleAPI.getErrorDialog(this, result, PLAY_SERVICES_RESOLUTION_REQUEST).show()
|
||||
viewModel.handleGoogleLoginResult(this, null) {
|
||||
handleAuthResponse(it)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun newGameButtonClicked() {
|
||||
|
|
@ -629,8 +439,6 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
|
|||
|
||||
companion object {
|
||||
internal const val REQUEST_CODE_PICK_ACCOUNT = 1000
|
||||
private const val REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR = 1001
|
||||
private const val PLAY_SERVICES_RESOLUTION_REQUEST = 9000
|
||||
|
||||
fun show(v: View) {
|
||||
v.visibility = View.VISIBLE
|
||||
|
|
|
|||
|
|
@ -61,9 +61,7 @@ class PrefsActivity : BaseActivity(), PreferenceFragmentCompat.OnPreferenceStart
|
|||
|
||||
private fun createNextPage(preferenceScreen: PreferenceScreen): PreferenceFragmentCompat? =
|
||||
when (preferenceScreen.key) {
|
||||
"profile" -> ProfilePreferencesFragment()
|
||||
"authentication" -> AuthenticationPreferenceFragment()
|
||||
"api" -> APIPreferenceFragment()
|
||||
"my_account" -> AccountPreferenceFragment()
|
||||
"pushNotifications" -> PushNotificationsPreferencesFragment()
|
||||
"emailNotifications" -> EmailNotificationsPreferencesFragment()
|
||||
else -> null
|
||||
|
|
|
|||
|
|
@ -68,16 +68,8 @@ class NavigationDrawerAdapter(tintColor: Int, backgroundTintColor: Int) : Recycl
|
|||
|
||||
fun updateItem(item: HabiticaDrawerItem) {
|
||||
val position = getItemPosition(item.identifier)
|
||||
if (position == -1) {
|
||||
items.add(item)
|
||||
notifyItemInserted(items.size - 1)
|
||||
} else {
|
||||
items[position] = item
|
||||
val visiblePosition = getVisibleItemPosition(item.identifier)
|
||||
if (visiblePosition in 0 until itemCount) {
|
||||
notifyItemChanged(visiblePosition)
|
||||
}
|
||||
}
|
||||
items[position] = item
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun updateItems(newItems: List<HabiticaDrawerItem>) {
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
package com.habitrpg.android.habitica.ui.fragments.preferences
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.preference.Preference
|
||||
import com.habitrpg.android.habitica.HabiticaBaseApplication
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.api.HostConfig
|
||||
import javax.inject.Inject
|
||||
|
||||
class APIPreferenceFragment : BasePreferencesFragment() {
|
||||
@Inject
|
||||
lateinit var hostConfig: HostConfig
|
||||
|
||||
private val apiPreferences: List<String>
|
||||
get() = listOf(getString(R.string.SP_APIToken), getString(R.string.SP_userID))
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
HabiticaBaseApplication.userComponent?.inject(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
val clipMan = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
clipMan?.setPrimaryClip(
|
||||
if (preference.key == getString(R.string.SP_APIToken)) {
|
||||
ClipData.newPlainText(preference.key, hostConfig.apiKey)
|
||||
} else {
|
||||
ClipData.newPlainText(preference.key, preference.summary)
|
||||
}
|
||||
)
|
||||
Toast.makeText(activity, "Copied " + preference.key + " to clipboard.", Toast.LENGTH_SHORT).show()
|
||||
return super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
override fun setupPreferences() {
|
||||
for ((key, value) in preferenceScreen.sharedPreferences.all) {
|
||||
if (apiPreferences.contains(key) && value != null) {
|
||||
findPreference<Preference>(key)?.summary = value.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +1,46 @@
|
|||
package com.habitrpg.android.habitica.ui.fragments.preferences
|
||||
|
||||
import android.accounts.AccountManager
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.habitrpg.android.habitica.HabiticaBaseApplication
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.api.HostConfig
|
||||
import com.habitrpg.android.habitica.data.ApiClient
|
||||
import com.habitrpg.android.habitica.extensions.addCancelButton
|
||||
import com.habitrpg.android.habitica.extensions.addCloseButton
|
||||
import com.habitrpg.android.habitica.extensions.dpToPx
|
||||
import com.habitrpg.android.habitica.extensions.layoutInflater
|
||||
import com.habitrpg.android.habitica.helpers.AppConfigManager
|
||||
import com.habitrpg.android.habitica.helpers.MainNavigationController
|
||||
import com.habitrpg.android.habitica.helpers.RxErrorHandler
|
||||
import com.habitrpg.android.habitica.models.user.User
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.AuthenticationViewModel
|
||||
import com.habitrpg.android.habitica.ui.views.ExtraLabelPreference
|
||||
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
|
||||
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaProgressDialog
|
||||
import com.habitrpg.android.habitica.ui.views.subscriptions.SubscriptionDetailsView
|
||||
import javax.inject.Inject
|
||||
|
||||
class AuthenticationPreferenceFragment : BasePreferencesFragment() {
|
||||
|
||||
class AccountPreferenceFragment: BasePreferencesFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
@Inject
|
||||
lateinit var configManager: AppConfigManager
|
||||
|
||||
lateinit var hostConfig: HostConfig
|
||||
@Inject
|
||||
lateinit var apiClient: ApiClient
|
||||
|
||||
private lateinit var viewModel: AuthenticationViewModel
|
||||
|
||||
override var user: User? = null
|
||||
set(value) {
|
||||
field = value
|
||||
|
|
@ -44,65 +50,184 @@ class AuthenticationPreferenceFragment : BasePreferencesFragment() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
HabiticaBaseApplication.userComponent?.inject(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel = AuthenticationViewModel()
|
||||
findPreference<Preference>("confirm_username")?.isVisible = user?.flags?.verifiedUsername == false
|
||||
|
||||
findPreference<Preference>("login_name")?.title = context?.getString(R.string.username)
|
||||
findPreference<Preference>("confirm_username")?.isVisible = user?.flags?.verifiedUsername != true
|
||||
}
|
||||
|
||||
private fun updateUserFields() {
|
||||
configurePreference(findPreference("login_name"), user?.authentication?.localAuthentication?.username, false)
|
||||
configurePreference(findPreference("email"), user?.authentication?.localAuthentication?.email, true)
|
||||
findPreference<Preference>("change_password")?.isVisible = user?.authentication?.localAuthentication?.email?.isNotEmpty() == true
|
||||
findPreference<Preference>("add_local_auth")?.isVisible = user?.authentication?.localAuthentication?.email?.isNotEmpty() != true
|
||||
findPreference<Preference>("confirm_username")?.isVisible = user?.flags?.verifiedUsername != true
|
||||
val preference = findPreference<Preference>("authentication_methods")
|
||||
val methods = mutableListOf<String>()
|
||||
if (user?.authentication?.localAuthentication?.email != null) {
|
||||
context?.getString(R.string.local)?.let { methods.add(it) }
|
||||
}
|
||||
if (user?.authentication?.hasFacebookAuth == true) { context?.getString(R.string.facebook)?.let { methods.add(it) } }
|
||||
if (user?.authentication?.hasGoogleAuth == true) { context?.getString(R.string.google)?.let { methods.add(it) } }
|
||||
if (user?.authentication?.hasAppleAuth == true) { context?.getString(R.string.apple_sign_in)?.let { methods.add(it) } }
|
||||
preference?.summary = methods.joinToString(", ")
|
||||
}
|
||||
|
||||
private fun configurePreference(preference: Preference?, value: String?, hideIfEmpty: Boolean) {
|
||||
preference?.summary = value
|
||||
if (hideIfEmpty) {
|
||||
preference?.isVisible = value?.isNotEmpty() == true
|
||||
}
|
||||
viewModel.setupFacebookLogin { viewModel.handleAuthResponse(it) }
|
||||
}
|
||||
|
||||
override fun setupPreferences() {
|
||||
updateUserFields()
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
when (preference.key) {
|
||||
"login_name" -> showLoginNameDialog()
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
|
||||
private fun updateUserFields() {
|
||||
val user = user ?: return
|
||||
configurePreference(findPreference("username"), user.authentication?.localAuthentication?.username)
|
||||
configurePreference(findPreference("email"), user.authentication?.localAuthentication?.email)
|
||||
findPreference<Preference>("confirm_username")?.isVisible = user.flags?.verifiedUsername != true
|
||||
|
||||
val passwordPref = findPreference<ExtraLabelPreference>("password")
|
||||
if (user.authentication?.hasPassword == true) {
|
||||
passwordPref?.summary = "··········"
|
||||
passwordPref?.extraText = getString(R.string.change_password)
|
||||
} else {
|
||||
passwordPref?.summary = getString(R.string.not_set)
|
||||
passwordPref?.extraText = getString(R.string.add_password)
|
||||
}
|
||||
val googlePref = findPreference<ExtraLabelPreference>("google_auth")
|
||||
if (user.authentication?.hasGoogleAuth == true) {
|
||||
googlePref?.summary = user.authentication?.googleAuthentication?.emails?.first()
|
||||
googlePref?.extraText = getString(R.string.disconnect)
|
||||
googlePref?.extraTextColor = context?.let { ContextCompat.getColor(it, R.color.text_red) }
|
||||
} else {
|
||||
googlePref?.summary = getString(R.string.not_connected)
|
||||
googlePref?.extraText = getString(R.string.connect)
|
||||
}
|
||||
val applePref = findPreference<ExtraLabelPreference>("apple_auth")
|
||||
if (user.authentication?.hasGoogleAuth == true) {
|
||||
applePref?.summary = user.authentication?.appleAuthentication?.emails?.first()
|
||||
applePref?.extraText = getString(R.string.disconnect)
|
||||
applePref?.extraTextColor = context?.let { ContextCompat.getColor(it, R.color.text_red) }
|
||||
} else {
|
||||
applePref?.summary = getString(R.string.not_connected)
|
||||
applePref?.extraText = getString(R.string.connect)
|
||||
}
|
||||
val facebookPref = findPreference<ExtraLabelPreference>("facebook_auth")
|
||||
if (user.authentication?.hasFacebookAuth == true) {
|
||||
facebookPref?.summary = user.authentication?.facebookAuthentication?.emails?.first()
|
||||
facebookPref?.extraText = getString(R.string.disconnect)
|
||||
facebookPref?.extraTextColor = context?.let { ContextCompat.getColor(it, R.color.text_red) }
|
||||
} else {
|
||||
facebookPref?.summary = getString(R.string.not_connected)
|
||||
facebookPref?.extraText = getString(R.string.connect)
|
||||
}
|
||||
|
||||
configurePreference(findPreference("display_name"), user.profile?.name)
|
||||
configurePreference(findPreference("photo_url"), user.profile?.imageUrl)
|
||||
configurePreference(findPreference("about"), user.profile?.blurb)
|
||||
|
||||
configurePreference(findPreference("UserID"), user.id)
|
||||
}
|
||||
|
||||
|
||||
private fun configurePreference(preference: Preference?, value: String?) {
|
||||
(preference as? EditTextPreference)?.let {
|
||||
it.text = value
|
||||
|
||||
}
|
||||
preference?.summary = value
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String) {
|
||||
val profileCategory = findPreference("profile") as? PreferenceCategory
|
||||
configurePreference(profileCategory?.findPreference(key), sharedPreferences?.getString(key, ""))
|
||||
if (sharedPreferences != null) {
|
||||
val newValue = sharedPreferences.getString(key, "") ?: return
|
||||
when (key) {
|
||||
"display_name" -> updateUser("profile.name", newValue, user?.profile?.name)
|
||||
"photo_url" -> updateUser("profile.imageUrl", newValue, user?.profile?.imageUrl)
|
||||
"about" -> updateUser("profile.blurb", newValue, user?.profile?.blurb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
|
||||
when(preference?.key) {
|
||||
"username" -> showLoginNameDialog()
|
||||
"confirm_username" -> showConfirmUsernameDialog()
|
||||
"email" -> showEmailDialog()
|
||||
"change_password" -> showChangePasswordDialog()
|
||||
"subscription_status" -> {
|
||||
val plan = user?.purchased?.plan
|
||||
if (plan?.isActive == true) {
|
||||
showSubscriptionStatusDialog()
|
||||
return super.onPreferenceTreeClick(preference)
|
||||
"password" -> {
|
||||
if (user?.authentication?.hasPassword == true) {
|
||||
showChangePasswordDialog()
|
||||
} else {
|
||||
showAddPasswordDialog()
|
||||
}
|
||||
}
|
||||
"UserID" -> {
|
||||
copyValue(getString(R.string.SP_userID), user?.id)
|
||||
return true
|
||||
}
|
||||
"ApiToken" -> {
|
||||
copyValue(getString(R.string.SP_APIToken_title), hostConfig.apiKey)
|
||||
return true
|
||||
}
|
||||
"google_auth" -> {
|
||||
if (user?.authentication?.hasGoogleAuth == true) {
|
||||
apiClient.disconnectSocial("google").subscribe({}, RxErrorHandler.handleEmptyError())
|
||||
} else {
|
||||
activity?.let {
|
||||
viewModel.handleGoogleLogin(it, pickAccountResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
"apple_auth" -> {
|
||||
if (user?.authentication?.hasAppleAuth == true) {
|
||||
apiClient.disconnectSocial("apple").subscribe({}, RxErrorHandler.handleEmptyError())
|
||||
} else {
|
||||
viewModel.connectApple(parentFragmentManager) {
|
||||
viewModel.handleAuthResponse(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
"facebook_auth" -> {
|
||||
if (user?.authentication?.hasFacebookAuth == true) {
|
||||
apiClient.disconnectSocial("facebook").subscribe({}, RxErrorHandler.handleEmptyError())
|
||||
} else {
|
||||
viewModel.handleFacebookLogin(this)
|
||||
}
|
||||
MainNavigationController.navigate(R.id.gemPurchaseActivity, bundleOf(Pair("openSubscription", true)))
|
||||
}
|
||||
"reset_account" -> showAccountResetConfirmation()
|
||||
"delete_account" -> showAccountDeleteConfirmation()
|
||||
"add_local_auth" -> showAddLocalAuthDialog()
|
||||
else -> {
|
||||
val clipMan = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
clipMan?.setPrimaryClip(ClipData.newPlainText(preference.key, preference.summary))
|
||||
Toast.makeText(activity, "Copied " + preference.key + " to clipboard.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
return super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
viewModel.onActivityResult(requestCode, resultCode, data) {
|
||||
viewModel.handleAuthResponse(it)
|
||||
}
|
||||
}
|
||||
|
||||
private val pickAccountResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
viewModel.googleEmail = it?.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
|
||||
activity?.let { it1 ->
|
||||
viewModel.handleGoogleLoginResult(it1, recoverFromPlayServicesErrorResult) {
|
||||
viewModel.handleAuthResponse(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val recoverFromPlayServicesErrorResult = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode != Activity.RESULT_CANCELED) {
|
||||
activity?.let { it1 ->
|
||||
viewModel.handleGoogleLoginResult(it1, null) {
|
||||
viewModel.handleAuthResponse(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUser(path: String, newValue: String, oldValue: String?) {
|
||||
if (newValue != oldValue) {
|
||||
compositeSubscription.add(userRepository.updateUser(path, newValue).subscribe({}, RxErrorHandler.handleEmptyError()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun showChangePasswordDialog() {
|
||||
val inflater = context?.layoutInflater
|
||||
val view = inflater?.inflate(R.layout.dialog_edittext_change_pw, null)
|
||||
|
|
@ -128,6 +253,33 @@ class AuthenticationPreferenceFragment : BasePreferencesFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun showAddPasswordDialog() {
|
||||
val inflater = context?.layoutInflater
|
||||
val view = inflater?.inflate(R.layout.dialog_edittext_change_pw, null)
|
||||
val oldPasswordEditText = view?.findViewById<EditText>(R.id.editText)
|
||||
oldPasswordEditText?.visibility = View.GONE
|
||||
val passwordEditText = view?.findViewById<EditText>(R.id.passwordEditText)
|
||||
val passwordRepeatEditText = view?.findViewById<EditText>(R.id.passwordRepeatEditText)
|
||||
context?.let { context ->
|
||||
val dialog = HabiticaAlertDialog(context)
|
||||
dialog.setTitle(R.string.add_password)
|
||||
dialog.addButton(R.string.add, true) { _, _ ->
|
||||
val email = user?.authentication?.findFirstSocialEmail()
|
||||
apiClient.registerUser(user?.username ?: "", email ?: "", passwordEditText?.text.toString(), passwordRepeatEditText?.text.toString())
|
||||
.subscribe(
|
||||
{
|
||||
Toast.makeText(activity, R.string.password_changed, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
RxErrorHandler.handleEmptyError()
|
||||
)
|
||||
}
|
||||
dialog.addCancelButton()
|
||||
dialog.setAdditionalContentView(view)
|
||||
dialog.setAdditionalContentSidePadding(12)
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEmailDialog() {
|
||||
val inflater = context?.layoutInflater
|
||||
val view = inflater?.inflate(R.layout.dialog_edittext_confirm_pw, null)
|
||||
|
|
@ -142,7 +294,7 @@ class AuthenticationPreferenceFragment : BasePreferencesFragment() {
|
|||
userRepository.updateEmail(emailEditText?.text.toString(), passwordEditText?.text.toString())
|
||||
.subscribe(
|
||||
{
|
||||
configurePreference(findPreference("email"), emailEditText?.text.toString(), true)
|
||||
configurePreference(findPreference("email"), emailEditText?.text.toString())
|
||||
},
|
||||
RxErrorHandler.handleEmptyError()
|
||||
)
|
||||
|
|
@ -167,7 +319,7 @@ class AuthenticationPreferenceFragment : BasePreferencesFragment() {
|
|||
userRepository.updateLoginName(loginNameEditText?.text.toString())
|
||||
.subscribe(
|
||||
{
|
||||
configurePreference(findPreference("login_name"), loginNameEditText?.text.toString(), true)
|
||||
configurePreference(findPreference("username"), loginNameEditText?.text.toString())
|
||||
},
|
||||
RxErrorHandler.handleEmptyError()
|
||||
)
|
||||
|
|
@ -205,36 +357,6 @@ class AuthenticationPreferenceFragment : BasePreferencesFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun showAddLocalAuthDialog() {
|
||||
val inflater = context?.layoutInflater
|
||||
val view = inflater?.inflate(R.layout.dialog_edittext_add_local_auth, null)
|
||||
val emailEditText = view?.findViewById<EditText>(R.id.emailTitleTextView)
|
||||
val passwordEditText = view?.findViewById<EditText>(R.id.passwordEditText)
|
||||
val passwordRepeatEditText = view?.findViewById<EditText>(R.id.passwordRepeatEditText)
|
||||
context?.let { context ->
|
||||
val dialog = HabiticaAlertDialog(context)
|
||||
dialog.setTitle(R.string.add_local_authentication)
|
||||
dialog.addButton(R.string.save, true) { thisDialog, _ ->
|
||||
if (passwordEditText?.text == passwordRepeatEditText?.text) {
|
||||
return@addButton
|
||||
}
|
||||
thisDialog.dismiss()
|
||||
apiClient.registerUser(user?.username ?: "", emailEditText?.text.toString(), passwordEditText?.text.toString(), passwordRepeatEditText?.text.toString())
|
||||
.flatMap { userRepository.retrieveUser(false) }
|
||||
.subscribe(
|
||||
{
|
||||
configurePreference(findPreference("email"), emailEditText?.text.toString(), true)
|
||||
},
|
||||
RxErrorHandler.handleEmptyError()
|
||||
)
|
||||
}
|
||||
dialog.addCancelButton()
|
||||
dialog.setAdditionalContentView(view)
|
||||
dialog.setAdditionalContentSidePadding(12.dpToPx(context))
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteAccount(password: String) {
|
||||
val dialog = HabiticaProgressDialog.show(context, R.string.deleting_account)
|
||||
compositeSubscription.add(
|
||||
|
|
@ -286,17 +408,8 @@ class AuthenticationPreferenceFragment : BasePreferencesFragment() {
|
|||
)
|
||||
}
|
||||
|
||||
private fun showSubscriptionStatusDialog() {
|
||||
context?.let { context ->
|
||||
val view = SubscriptionDetailsView(context)
|
||||
user?.purchased?.plan?.let {
|
||||
view.setPlan(it)
|
||||
}
|
||||
val dialog = HabiticaAlertDialog(context)
|
||||
dialog.setAdditionalContentView(view)
|
||||
dialog.setTitle(R.string.subscription_status)
|
||||
dialog.addCloseButton()
|
||||
dialog.show()
|
||||
}
|
||||
private fun copyValue(name: String, value: CharSequence?) {
|
||||
ClipData.newPlainText(name, value)
|
||||
Toast.makeText(activity, "Copied $name to clipboard.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
package com.habitrpg.android.habitica.ui.fragments.preferences
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import com.habitrpg.android.habitica.HabiticaBaseApplication
|
||||
import com.habitrpg.android.habitica.helpers.RxErrorHandler
|
||||
import com.habitrpg.android.habitica.models.user.User
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
|
||||
class ProfilePreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
override var user: User? = null
|
||||
set(value) {
|
||||
field = value
|
||||
updateUserFields()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
HabiticaBaseApplication.userComponent?.inject(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun updateUserFields() {
|
||||
configurePreference(findPreference("display_name"), user?.profile?.name)
|
||||
configurePreference(findPreference("photo_url"), user?.profile?.imageUrl)
|
||||
configurePreference(findPreference("about"), user?.profile?.blurb)
|
||||
}
|
||||
|
||||
private fun configurePreference(preference: Preference?, value: String?) {
|
||||
val editPreference = preference as? EditTextPreference
|
||||
editPreference?.text = value
|
||||
preference?.summary = value
|
||||
}
|
||||
|
||||
override fun setupPreferences() {
|
||||
updateUserFields()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String) {
|
||||
val profileCategory = findPreference("profile") as? PreferenceCategory
|
||||
configurePreference(profileCategory?.findPreference(key), sharedPreferences?.getString(key, ""))
|
||||
if (sharedPreferences != null) {
|
||||
val newValue = sharedPreferences.getString(key, "") ?: return
|
||||
val observable: Flowable<User>? = when (key) {
|
||||
"display_name" -> {
|
||||
if (newValue != user?.profile?.name) {
|
||||
userRepository.updateUser("profile.name", newValue)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
"photo_url" -> {
|
||||
if (newValue != user?.profile?.imageUrl) {
|
||||
userRepository.updateUser("profile.imageUrl", newValue)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
"about" -> {
|
||||
if (newValue != user?.profile?.blurb) {
|
||||
userRepository.updateUser("profile.blurb", newValue)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
observable?.subscribe({}, RxErrorHandler.handleEmptyError())?.let { compositeSubscription.add(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,13 +34,18 @@ class GiftBalanceGemsFragment : BaseFragment<FragmentGiftGemBalanceBinding>() {
|
|||
set(value) {
|
||||
field = value
|
||||
field?.let {
|
||||
binding?.avatarView?.setAvatar(it)
|
||||
binding?.displayNameTextview?.username = it.profile?.name
|
||||
binding?.displayNameTextview?.tier = it.contributor?.level ?: 0
|
||||
binding?.usernameTextview?.text = "@${it.username}"
|
||||
updateMemberViews()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMemberViews() {
|
||||
val it = giftedMember ?: return
|
||||
binding?.avatarView?.setAvatar(it)
|
||||
binding?.displayNameTextview?.username = it.profile?.name
|
||||
binding?.displayNameTextview?.tier = it.contributor?.level ?: 0
|
||||
binding?.usernameTextview?.text = it.formattedUsername
|
||||
}
|
||||
|
||||
var onCompleted: (() -> Unit)? = null
|
||||
|
||||
override fun injectFragment(component: UserComponent) {
|
||||
|
|
@ -50,6 +55,7 @@ class GiftBalanceGemsFragment : BaseFragment<FragmentGiftGemBalanceBinding>() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding?.giftButton?.setOnClickListener { sendGift() }
|
||||
updateMemberViews()
|
||||
}
|
||||
|
||||
private fun sendGift() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,288 @@
|
|||
package com.habitrpg.android.habitica.ui.viewmodels
|
||||
|
||||
import android.accounts.AccountManager
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.facebook.AccessToken
|
||||
import com.facebook.CallbackManager
|
||||
import com.facebook.FacebookCallback
|
||||
import com.facebook.FacebookException
|
||||
import com.facebook.FacebookSdk
|
||||
import com.facebook.login.LoginManager
|
||||
import com.facebook.login.LoginResult
|
||||
import com.google.android.gms.auth.GoogleAuthException
|
||||
import com.google.android.gms.auth.GoogleAuthUtil
|
||||
import com.google.android.gms.auth.GooglePlayServicesAvailabilityException
|
||||
import com.google.android.gms.auth.UserRecoverableAuthException
|
||||
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.google.android.gms.common.UserRecoverableException
|
||||
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
|
||||
import com.habitrpg.android.habitica.data.ApiClient
|
||||
import com.habitrpg.android.habitica.extensions.addCloseButton
|
||||
import com.habitrpg.android.habitica.helpers.KeyHelper
|
||||
import com.habitrpg.android.habitica.helpers.RxErrorHandler
|
||||
import com.habitrpg.android.habitica.helpers.SignInWithAppleResult
|
||||
import com.habitrpg.android.habitica.helpers.SignInWithAppleService
|
||||
import com.habitrpg.android.habitica.models.auth.UserAuthResponse
|
||||
import com.habitrpg.android.habitica.proxy.AnalyticsManager
|
||||
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
|
||||
import com.willowtreeapps.signinwithapplebutton.SignInWithAppleConfiguration
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.exceptions.Exceptions
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class AuthenticationViewModel() {
|
||||
@Inject
|
||||
internal lateinit var apiClient: ApiClient
|
||||
@Inject
|
||||
internal lateinit var sharedPrefs: SharedPreferences
|
||||
@Inject
|
||||
internal lateinit var hostConfig: HostConfig
|
||||
@Inject
|
||||
internal lateinit var analyticsManager: AnalyticsManager
|
||||
@Inject
|
||||
@JvmField
|
||||
var keyHelper: KeyHelper? = null
|
||||
|
||||
private var compositeSubscription = CompositeDisposable()
|
||||
|
||||
|
||||
private var callbackManager = CallbackManager.Factory.create()
|
||||
var googleEmail: String? = null
|
||||
private var loginManager = LoginManager.getInstance()
|
||||
|
||||
init {
|
||||
HabiticaBaseApplication.userComponent?.inject(this)
|
||||
}
|
||||
|
||||
fun connectApple(fragmentManager: FragmentManager, onSuccess: (UserAuthResponse) -> Unit) {
|
||||
val configuration = SignInWithAppleConfiguration(
|
||||
clientId = BuildConfig.APPLE_AUTH_CLIENT_ID,
|
||||
redirectUri = "${hostConfig.address}/api/v4/user/auth/apple",
|
||||
scope = "name email"
|
||||
)
|
||||
val fragmentTag = "SignInWithAppleButton-SignInWebViewDialogFragment"
|
||||
|
||||
SignInWithAppleService(fragmentManager, fragmentTag, configuration) { result ->
|
||||
when (result) {
|
||||
is SignInWithAppleResult.Success -> {
|
||||
val response = UserAuthResponse()
|
||||
response.id = result.userID
|
||||
response.apiToken = result.apiKey
|
||||
response.newUser = result.newUser
|
||||
onSuccess(response)
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
fun setupFacebookLogin(onSuccess: (UserAuthResponse) -> Unit) {
|
||||
callbackManager = CallbackManager.Factory.create()
|
||||
loginManager.registerCallback(
|
||||
callbackManager,
|
||||
object : FacebookCallback<LoginResult> {
|
||||
override fun onSuccess(loginResult: LoginResult) {
|
||||
val accessToken = AccessToken.getCurrentAccessToken()
|
||||
compositeSubscription.add(
|
||||
apiClient.connectSocial("facebook", accessToken?.userId ?: "", accessToken?.token ?: "")
|
||||
.subscribe({
|
||||
onSuccess(it)
|
||||
}, RxErrorHandler.handleEmptyError())
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCancel() { /* no-on */ }
|
||||
|
||||
override fun onError(exception: FacebookException) {
|
||||
RxErrorHandler.reportError(exception)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun handleFacebookLogin(activity: Activity) {
|
||||
loginManager.logInWithReadPermissions(activity, listOf("user_friends"))
|
||||
}
|
||||
|
||||
fun handleFacebookLogin(fragment: Fragment) {
|
||||
loginManager.logInWithReadPermissions(fragment, listOf("user_friends"))
|
||||
}
|
||||
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, onSuccess: (UserAuthResponse) -> Unit) {
|
||||
callbackManager.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (requestCode == FacebookSdk.getCallbackRequestCodeOffset()) {
|
||||
// This is necessary because the regular login callback is not called for some reason
|
||||
val accessToken = AccessToken.getCurrentAccessToken()
|
||||
if (accessToken?.token != null) {
|
||||
compositeSubscription.add(
|
||||
apiClient.connectSocial("facebook", accessToken.userId, accessToken.token)
|
||||
.subscribe({
|
||||
onSuccess(it)
|
||||
}, { })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleGoogleLogin(
|
||||
activity: Activity,
|
||||
pickAccountResult: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
if (!checkPlayServices(activity)) {
|
||||
return
|
||||
}
|
||||
val accountTypes = arrayOf("com.google")
|
||||
val intent = AccountManager.newChooseAccountIntent(
|
||||
null, null,
|
||||
accountTypes, true, null, null, null, null
|
||||
)
|
||||
try {
|
||||
pickAccountResult.launch(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
val alert = HabiticaAlertDialog(activity)
|
||||
alert.setTitle(R.string.authentication_error_title)
|
||||
alert.setMessage(R.string.google_services_missing)
|
||||
alert.addCloseButton()
|
||||
alert.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleGoogleLoginResult(
|
||||
activity: Activity,
|
||||
recoverFromPlayServicesErrorResult: ActivityResultLauncher<Intent>?,
|
||||
onSuccess: (UserAuthResponse) -> Unit
|
||||
) {
|
||||
val scopesString = Scopes.PROFILE + " " + Scopes.EMAIL
|
||||
val scopes = "oauth2:$scopesString"
|
||||
compositeSubscription.add(
|
||||
Flowable.defer {
|
||||
try {
|
||||
@Suppress("Deprecation")
|
||||
return@defer Flowable.just(GoogleAuthUtil.getToken(activity, googleEmail, scopes))
|
||||
} catch (e: IOException) {
|
||||
throw Exceptions.propagate(e)
|
||||
} catch (e: GoogleAuthException) {
|
||||
throw Exceptions.propagate(e)
|
||||
} catch (e: UserRecoverableException) {
|
||||
return@defer Flowable.empty()
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap { token -> apiClient.connectSocial("google", googleEmail ?: "", token) }
|
||||
.subscribe(
|
||||
{
|
||||
onSuccess(it)
|
||||
},
|
||||
{ throwable ->
|
||||
if (recoverFromPlayServicesErrorResult == null) return@subscribe
|
||||
throwable.cause?.let {
|
||||
if (GoogleAuthException::class.java.isAssignableFrom(it.javaClass)) {
|
||||
handleGoogleAuthException(throwable.cause as GoogleAuthException,
|
||||
activity,
|
||||
recoverFromPlayServicesErrorResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleGoogleAuthException(
|
||||
e: Exception,
|
||||
activity: Activity,
|
||||
recoverFromPlayServicesErrorResult: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
if (e is GooglePlayServicesAvailabilityException) {
|
||||
// The Google Play services APK is old, disabled, or not present.
|
||||
// Show a dialog created by Google Play services that allows
|
||||
// the user to update the APK
|
||||
val statusCode = e
|
||||
.connectionStatusCode
|
||||
GoogleApiAvailability.getInstance()
|
||||
@Suppress("DEPRECATION")
|
||||
GooglePlayServicesUtil.showErrorDialogFragment(
|
||||
statusCode,
|
||||
activity,
|
||||
AuthenticationViewModel.REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR
|
||||
) {
|
||||
}
|
||||
} else if (e is UserRecoverableAuthException) {
|
||||
// Unable to authenticate, such as when the user has not yet granted
|
||||
// the app access to the account, but the user can fix this.
|
||||
// Forward the user to an activity in Google Play services.
|
||||
val intent = e.intent
|
||||
recoverFromPlayServicesErrorResult.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun checkPlayServices(activity: Activity): Boolean {
|
||||
val googleAPI = GoogleApiAvailability.getInstance()
|
||||
val result = googleAPI.isGooglePlayServicesAvailable(activity)
|
||||
if (result != ConnectionResult.SUCCESS) {
|
||||
if (googleAPI.isUserResolvableError(result)) {
|
||||
googleAPI.getErrorDialog(activity, result,
|
||||
AuthenticationViewModel.PLAY_SERVICES_RESOLUTION_REQUEST
|
||||
).show()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun handleAuthResponse(userAuthResponse: UserAuthResponse) {
|
||||
try {
|
||||
saveTokens(userAuthResponse.apiToken, userAuthResponse.id)
|
||||
} catch (e: Exception) {
|
||||
analyticsManager.logException(e)
|
||||
}
|
||||
|
||||
HabiticaBaseApplication.reloadUserComponent()
|
||||
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private fun saveTokens(api: String, user: String) {
|
||||
this.apiClient.updateAuthenticationCredentials(user, api)
|
||||
sharedPrefs.edit {
|
||||
putString("UserID", user)
|
||||
val encryptedKey = if (keyHelper != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
keyHelper?.encrypt(api)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
} else null
|
||||
if (encryptedKey?.length ?: 0 > 5) {
|
||||
putString(user, encryptedKey)
|
||||
} else {
|
||||
// Something might have gone wrong with encryption, so fall back to this.
|
||||
putString("ApiToken", api)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR = 1001
|
||||
private const val PLAY_SERVICES_RESOLUTION_REQUEST = 9000
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.habitrpg.android.habitica.ui.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.TextView
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import com.habitrpg.android.habitica.R
|
||||
|
||||
class ExtraLabelPreference(
|
||||
context: Context?,
|
||||
attrs: AttributeSet?
|
||||
) : Preference(context, attrs) {
|
||||
var extraText: String? = null
|
||||
var extraTextColor: Int? = null
|
||||
|
||||
init {
|
||||
layoutResource = R.layout.preference_button
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
|
||||
super.onBindViewHolder(holder)
|
||||
val textView = holder?.itemView?.findViewById<TextView>(R.id.extra_label)
|
||||
textView?.text = extraText
|
||||
extraTextColor?.let {
|
||||
textView?.setTextColor(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -105,15 +105,6 @@ class UserDeserializer : JsonDeserializer<User> {
|
|||
}
|
||||
if (obj.has("auth")) {
|
||||
user.authentication = context.deserialize(obj.get("auth"), Authentication::class.java)
|
||||
if (obj.getAsJsonObject("auth").has("facebook") && obj.getAsJsonObject("auth").getAsJsonObject("facebook").has("emails")) {
|
||||
user.authentication?.hasFacebookAuth = true
|
||||
}
|
||||
if (obj.getAsJsonObject("auth").has("google") && obj.getAsJsonObject("auth").getAsJsonObject("google").has("emails")) {
|
||||
user.authentication?.hasGoogleAuth = true
|
||||
}
|
||||
if (obj.getAsJsonObject("auth").has("apple") && obj.getAsJsonObject("auth").getAsJsonObject("apple").has("email")) {
|
||||
user.authentication?.hasAppleAuth = true
|
||||
}
|
||||
}
|
||||
if (obj.has("flags")) {
|
||||
user.flags = context.deserialize(obj.get("flags"), Flags::class.java)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.5.30'
|
||||
ext.kotlin_version = '1.5.31'
|
||||
|
||||
repositories {
|
||||
google()
|
||||
|
|
@ -9,10 +9,10 @@ buildscript {
|
|||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.2'
|
||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
|
||||
classpath 'com.google.gms:google-services:4.3.10'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.0'
|
||||
classpath "io.realm:realm-gradle-plugin:10.8.0"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.18.1"
|
||||
|
|
|
|||
Loading…
Reference in a new issue