diff --git a/README.md b/README.md index c656407c1..1bcd3f4e2 100644 --- a/README.md +++ b/README.md @@ -47,16 +47,16 @@ libraries. ```groovy dependencies { // FirebaseUI for Firebase Realtime Database - implementation 'com.firebaseui:firebase-ui-database:4.0.1' + implementation 'com.firebaseui:firebase-ui-database:4.1.0' // FirebaseUI for Cloud Firestore - implementation 'com.firebaseui:firebase-ui-firestore:4.0.1' + implementation 'com.firebaseui:firebase-ui-firestore:4.1.0' // FirebaseUI for Firebase Auth - implementation 'com.firebaseui:firebase-ui-auth:4.0.1' + implementation 'com.firebaseui:firebase-ui-auth:4.1.0' // FirebaseUI for Cloud Storage - implementation 'com.firebaseui:firebase-ui-storage:4.0.1' + implementation 'com.firebaseui:firebase-ui-storage:4.1.0' } ``` @@ -99,7 +99,7 @@ versions. This means that FirebaseUI has independent dependencies on each of the For best results, your app should depend on a version of each dependency with the same major version number as the version used by FirebaseUI. -As of version `4.0.0`, FirebaseUI has the following dependency versions: +As of version `4.1.0`, FirebaseUI has the following dependency versions: | Library | Version | |----------------------|--------------------------------| diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0a381a18c..0a6d84862 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,6 +37,9 @@ + { private static final Class[] CLASSES = new Class[]{ AuthUiActivity.class, + AnonymousUpgradeActivity.class, FirestoreChatActivity.class, FirestorePagingActivity.class, RealtimeDbChatActivity.class, @@ -61,6 +63,7 @@ private static class ActivityChooserAdapter extends RecyclerView.Adapter() { + @Override + public void onComplete(@NonNull Task task) { + updateUI(); + + if (task.isSuccessful()) { + setStatus("Signed in anonymously as user " + + getUserIdentifier(task.getResult().getUser())); + } else { + setStatus("Anonymous sign in failed."); + } + } + }); + } + + @OnClick(R.id.begin_flow) + public void startAuthUI() { + List providers = ConfigurationUtils.getConfiguredProviders(this); + Intent intent = AuthUI.getInstance().createSignInIntentBuilder() + .setLogo(R.drawable.firebase_auth_120dp) + .setAvailableProviders(providers) + .setIsSmartLockEnabled(false) + .enableAnonymousUsersAutoUpgrade() + .build(); + startActivityForResult(intent, RC_SIGN_IN); + } + + @OnClick(R.id.resolve_merge) + public void resolveMerge() { + if (mPendingCredential == null) { + Toast.makeText(this, "Nothing to resolve.", Toast.LENGTH_SHORT).show(); + return; + } + + // TODO: Show how to do good data moving + + FirebaseAuth.getInstance().signInWithCredential(mPendingCredential) + .addOnCompleteListener(this, new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + mPendingCredential = null; + updateUI(); + + if (task.isSuccessful()) { + setStatus("Signed in as " + getUserIdentifier(task.getResult().getUser())); + } else { + Log.w(TAG, "Merge failed", task.getException()); + setStatus("Failed to resolve merge conflict, see logs."); + } + } + }); + } + + @OnClick(R.id.sign_out) + public void signOut() { + AuthUI.getInstance().signOut(this) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + setStatus(null); + updateUI(); + } + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == RC_SIGN_IN) { + IdpResponse response = IdpResponse.fromResultIntent(data); + if (response == null) { + // User pressed back button + return; + } + if (resultCode == RESULT_OK) { + setStatus("Signed in as " + getUserIdentifier(FirebaseAuth.getInstance().getCurrentUser())); + } else { + if (response.getError().getErrorCode() == ErrorCodes.ANONYMOUS_UPGRADE_MERGE_CONFLICT) { + setStatus("Merge conflict: user already exists."); + mResolveMergeButton.setEnabled(true); + mPendingCredential = response.getCredentialForLinking(); + } + } + + updateUI(); + } + } + + private void updateUI() { + FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser(); + + if (currentUser == null) { + // Not signed in + mAnonSignInButton.setEnabled(true); + mLaunchUIButton.setEnabled(false); + mResolveMergeButton.setEnabled(false); + mSignOutButton.setEnabled(false); + } else if (mPendingCredential == null && currentUser.isAnonymous()) { + // Anonymous user, waiting for linking + mAnonSignInButton.setEnabled(false); + mLaunchUIButton.setEnabled(true); + mResolveMergeButton.setEnabled(false); + mSignOutButton.setEnabled(true); + } else if (mPendingCredential == null && !currentUser.isAnonymous()) { + // Fully signed in + mAnonSignInButton.setEnabled(false); + mLaunchUIButton.setEnabled(false); + mResolveMergeButton.setEnabled(false); + mSignOutButton.setEnabled(true); + } else if (mPendingCredential != null) { + // Signed in anonymous, awaiting merge conflict + mAnonSignInButton.setEnabled(false); + mLaunchUIButton.setEnabled(false); + mResolveMergeButton.setEnabled(true); + mSignOutButton.setEnabled(true); + } + } + + private void setStatus(String message) { + mStatus.setText(message); + } + + private String getUserIdentifier(FirebaseUser user) { + if (user.isAnonymous()) { + return user.getUid(); + } else if (!TextUtils.isEmpty(user.getEmail())) { + return user.getEmail(); + } else if (!TextUtils.isEmpty(user.getPhoneNumber())) { + return user.getPhoneNumber(); + } else { + return "unknown"; + } + } +} diff --git a/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java b/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java index f98a4d7fa..c1aeb0632 100644 --- a/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java +++ b/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java @@ -38,6 +38,7 @@ import com.firebase.ui.auth.ErrorCodes; import com.firebase.ui.auth.IdpResponse; import com.firebase.uidemo.R; +import com.firebase.uidemo.util.ConfigurationUtils; import com.google.android.gms.common.Scopes; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; @@ -45,7 +46,6 @@ import com.google.firebase.auth.FirebaseAuth; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import butterknife.BindView; @@ -67,13 +67,14 @@ public class AuthUiActivity extends AppCompatActivity { @BindView(R.id.google_provider) CheckBox mUseGoogleProvider; @BindView(R.id.facebook_provider) CheckBox mUseFacebookProvider; @BindView(R.id.twitter_provider) CheckBox mUseTwitterProvider; + @BindView(R.id.github_provider) CheckBox mUseGitHubProvider; @BindView(R.id.email_provider) CheckBox mUseEmailProvider; @BindView(R.id.phone_provider) CheckBox mUsePhoneProvider; - @BindView(R.id.default_theme) RadioButton mUseDefaultTheme; - @BindView(R.id.green_theme) RadioButton mUseGreenTheme; - @BindView(R.id.purple_theme) RadioButton mUsePurpleTheme; - @BindView(R.id.dark_theme) RadioButton mUseDarkTheme; + @BindView(R.id.default_theme) RadioButton mDefaultTheme; + @BindView(R.id.green_theme) RadioButton mGreenTheme; + @BindView(R.id.purple_theme) RadioButton mPurpleTheme; + @BindView(R.id.dark_theme) RadioButton mDarkTheme; @BindView(R.id.firebase_logo) RadioButton mFirebaseLogo; @BindView(R.id.google_logo) RadioButton mGoogleLogo; @@ -85,13 +86,17 @@ public class AuthUiActivity extends AppCompatActivity { @BindView(R.id.google_privacy) RadioButton mUseGooglePrivacyPolicy; @BindView(R.id.firebase_privacy) RadioButton mUseFirebasePrivacyPolicy; - @BindView(R.id.google_scopes_header) TextView mGoogleScopesLabel; + @BindView(R.id.google_scopes_header) TextView mGoogleScopesHeader; @BindView(R.id.google_scope_drive_file) CheckBox mGoogleScopeDriveFile; @BindView(R.id.google_scope_youtube_data) CheckBox mGoogleScopeYoutubeData; - @BindView(R.id.facebook_permissions_header) TextView mFacebookScopesLabel; - @BindView(R.id.facebook_permission_friends) CheckBox mFacebookScopeFriends; - @BindView(R.id.facebook_permission_photos) CheckBox mFacebookScopePhotos; + @BindView(R.id.facebook_permissions_header) TextView mFacebookPermissionsHeader; + @BindView(R.id.facebook_permission_friends) CheckBox mFacebookPermissionFriends; + @BindView(R.id.facebook_permission_photos) CheckBox mFacebookPermissionPhotos; + + @BindView(R.id.github_permissions_header) TextView mGitHubPermissionsHeader; + @BindView(R.id.github_permission_repo) CheckBox mGitHubPermissionRepo; + @BindView(R.id.github_permission_gist) CheckBox mGitHubPermissionGist; @BindView(R.id.credential_selector_enabled) CheckBox mEnableCredentialSelector; @BindView(R.id.hint_selector_enabled) CheckBox mEnableHintSelector; @@ -108,7 +113,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { setContentView(R.layout.auth_ui_layout); ButterKnife.bind(this); - if (isGoogleMisconfigured()) { + if (ConfigurationUtils.isGoogleMisconfigured(this)) { mUseGoogleProvider.setChecked(false); mUseGoogleProvider.setEnabled(false); mUseGoogleProvider.setText(R.string.google_label_missing_config); @@ -123,33 +128,51 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { }); } - if (isFacebookMisconfigured()) { + if (ConfigurationUtils.isFacebookMisconfigured(this)) { mUseFacebookProvider.setChecked(false); mUseFacebookProvider.setEnabled(false); mUseFacebookProvider.setText(R.string.facebook_label_missing_config); - setFacebookScopesEnabled(false); + setFacebookPermissionsEnabled(false); } else { - setFacebookScopesEnabled(mUseFacebookProvider.isChecked()); + setFacebookPermissionsEnabled(mUseFacebookProvider.isChecked()); mUseFacebookProvider.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { - setFacebookScopesEnabled(checked); + setFacebookPermissionsEnabled(checked); } }); } - if (isTwitterMisconfigured()) { + if (ConfigurationUtils.isTwitterMisconfigured(this)) { mUseTwitterProvider.setChecked(false); mUseTwitterProvider.setEnabled(false); mUseTwitterProvider.setText(R.string.twitter_label_missing_config); } - if (isGoogleMisconfigured() || isFacebookMisconfigured() || isTwitterMisconfigured()) { + if (ConfigurationUtils.isGitHubMisconfigured(this)) { + mUseGitHubProvider.setChecked(false); + mUseGitHubProvider.setEnabled(false); + mUseGitHubProvider.setText(R.string.github_label_missing_config); + setGitHubPermissionsEnabled(false); + } else { + setGitHubPermissionsEnabled(mUseGitHubProvider.isChecked()); + mUseGitHubProvider.setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + setGitHubPermissionsEnabled(checked); + } + }); + } + + if (ConfigurationUtils.isGoogleMisconfigured(this) + || ConfigurationUtils.isFacebookMisconfigured(this) + || ConfigurationUtils.isTwitterMisconfigured(this) + || ConfigurationUtils.isGitHubMisconfigured(this)) { showSnackbar(R.string.configuration_required); } if (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES) { - mUseDarkTheme.setChecked(true); + mDarkTheme.setChecked(true); } } @@ -160,8 +183,8 @@ public void signIn(View view) { .setTheme(getSelectedTheme()) .setLogo(getSelectedLogo()) .setAvailableProviders(getSelectedProviders()) - .setTosUrl(getSelectedTosUrl()) - .setPrivacyPolicyUrl(getSelectedPrivacyPolicyUrl()) + .setTosAndPrivacyPolicyUrls(getSelectedTosUrl(), + getSelectedPrivacyPolicyUrl()) .setIsSmartLockEnabled(mEnableCredentialSelector.isChecked(), mEnableHintSelector.isChecked()) .build(), @@ -202,7 +225,7 @@ protected void onResume() { } private void handleSignInResponse(int resultCode, Intent data) { - IdpResponse response = IdpResponse.fromResultIntent(data); + final IdpResponse response = IdpResponse.fromResultIntent(data); // Successfully signed in if (resultCode == RESULT_OK) { @@ -232,7 +255,7 @@ private void startSignedInActivity(IdpResponse response) { @OnClick({R.id.default_theme, R.id.purple_theme, R.id.green_theme, R.id.dark_theme}) public void toggleDarkTheme() { - int mode = mUseDarkTheme.isChecked() ? + int mode = mDarkTheme.isChecked() ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_AUTO; AppCompatDelegate.setDefaultNightMode(mode); getDelegate().setLocalNightMode(mode); @@ -240,11 +263,11 @@ public void toggleDarkTheme() { @StyleRes private int getSelectedTheme() { - if (mUseGreenTheme.isChecked()) { + if (mGreenTheme.isChecked()) { return R.style.GreenTheme; } - if (mUsePurpleTheme.isChecked()) { + if (mPurpleTheme.isChecked()) { return R.style.PurpleTheme; } @@ -279,6 +302,12 @@ private List getSelectedProviders() { selectedProviders.add(new IdpConfig.TwitterBuilder().build()); } + if (mUseGitHubProvider.isChecked()) { + selectedProviders.add(new IdpConfig.GitHubBuilder() + .setPermissions(getGitHubPermissions()) + .build()); + } + if (mUseEmailProvider.isChecked()) { selectedProviders.add(new IdpConfig.EmailBuilder() .setRequireName(mRequireName.isChecked()) @@ -309,33 +338,22 @@ private String getSelectedPrivacyPolicyUrl() { return FIREBASE_PRIVACY_POLICY_URL; } - private boolean isGoogleMisconfigured() { - return AuthUI.UNCONFIGURED_CONFIG_VALUE.equals(getString(R.string.default_web_client_id)); - } - - private boolean isFacebookMisconfigured() { - return AuthUI.UNCONFIGURED_CONFIG_VALUE.equals(getString(R.string.facebook_application_id)); - } - - private boolean isTwitterMisconfigured() { - List twitterConfigs = Arrays.asList( - getString(R.string.twitter_consumer_key), - getString(R.string.twitter_consumer_secret) - ); - - return twitterConfigs.contains(AuthUI.UNCONFIGURED_CONFIG_VALUE); - } - private void setGoogleScopesEnabled(boolean enabled) { - mGoogleScopesLabel.setEnabled(enabled); + mGoogleScopesHeader.setEnabled(enabled); mGoogleScopeDriveFile.setEnabled(enabled); mGoogleScopeYoutubeData.setEnabled(enabled); } - private void setFacebookScopesEnabled(boolean enabled) { - mFacebookScopesLabel.setEnabled(enabled); - mFacebookScopeFriends.setEnabled(enabled); - mFacebookScopePhotos.setEnabled(enabled); + private void setFacebookPermissionsEnabled(boolean enabled) { + mFacebookPermissionsHeader.setEnabled(enabled); + mFacebookPermissionFriends.setEnabled(enabled); + mFacebookPermissionPhotos.setEnabled(enabled); + } + + private void setGitHubPermissionsEnabled(boolean enabled) { + mGitHubPermissionsHeader.setEnabled(enabled); + mGitHubPermissionRepo.setEnabled(enabled); + mGitHubPermissionGist.setEnabled(enabled); } private List getGoogleScopes() { @@ -351,15 +369,26 @@ private List getGoogleScopes() { private List getFacebookPermissions() { List result = new ArrayList<>(); - if (mFacebookScopeFriends.isChecked()) { + if (mFacebookPermissionFriends.isChecked()) { result.add("user_friends"); } - if (mFacebookScopePhotos.isChecked()) { + if (mFacebookPermissionPhotos.isChecked()) { result.add("user_photos"); } return result; } + private List getGitHubPermissions() { + List result = new ArrayList<>(); + if (mGitHubPermissionRepo.isChecked()) { + result.add("repo"); + } + if (mGitHubPermissionGist.isChecked()) { + result.add("gist"); + } + return result; + } + private void showSnackbar(@StringRes int errorMessageRes) { Snackbar.make(mRootView, errorMessageRes, Snackbar.LENGTH_LONG).show(); } diff --git a/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java b/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java index 6917ea8e5..f921f310c 100644 --- a/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java +++ b/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java @@ -42,6 +42,7 @@ import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuthProvider; import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.auth.GithubAuthProvider; import com.google.firebase.auth.GoogleAuthProvider; import com.google.firebase.auth.PhoneAuthProvider; import com.google.firebase.auth.TwitterAuthProvider; @@ -64,6 +65,7 @@ public class SignedInActivity extends AppCompatActivity { @BindView(R.id.user_display_name) TextView mUserDisplayName; @BindView(R.id.user_phone_number) TextView mUserPhoneNumber; @BindView(R.id.user_enabled_providers) TextView mEnabledProviders; + @BindView(R.id.user_is_new) TextView mIsNewUser; public static Intent createIntent(Context context, IdpResponse idpResponse) { return new Intent().setClass(context, SignedInActivity.class) @@ -85,7 +87,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { setContentView(R.layout.signed_in_layout); ButterKnife.bind(this); - populateProfile(); + populateProfile(response); populateIdpToken(response); } @@ -137,7 +139,7 @@ public void onComplete(@NonNull Task task) { }); } - private void populateProfile() { + private void populateProfile(@Nullable IdpResponse response) { FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser(); if (user.getPhotoUrl() != null) { GlideApp.with(this) @@ -153,6 +155,13 @@ private void populateProfile() { mUserDisplayName.setText( TextUtils.isEmpty(user.getDisplayName()) ? "No display name" : user.getDisplayName()); + if (response == null) { + mIsNewUser.setVisibility(View.GONE); + } else { + mIsNewUser.setVisibility(View.VISIBLE); + mIsNewUser.setText(response.isNewUser() ? "New user" : "Existing user"); + } + List providers = new ArrayList<>(); if (user.getProviderData().isEmpty()) { providers.add("Anonymous"); @@ -168,6 +177,9 @@ private void populateProfile() { case TwitterAuthProvider.PROVIDER_ID: providers.add(getString(R.string.providers_twitter)); break; + case GithubAuthProvider.PROVIDER_ID: + providers.add(getString(R.string.providers_github)); + break; case EmailAuthProvider.PROVIDER_ID: providers.add(getString(R.string.providers_email)); break; diff --git a/app/src/main/java/com/firebase/uidemo/util/ConfigurationUtils.java b/app/src/main/java/com/firebase/uidemo/util/ConfigurationUtils.java new file mode 100644 index 000000000..18f3f7d0b --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/util/ConfigurationUtils.java @@ -0,0 +1,68 @@ +package com.firebase.uidemo.util; + + +import android.content.Context; +import android.content.res.Resources; + +import com.firebase.ui.auth.AuthUI; +import com.firebase.uidemo.R; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class ConfigurationUtils { + + public static boolean isGoogleMisconfigured(Context context) { + return AuthUI.UNCONFIGURED_CONFIG_VALUE.equals( + context.getString(R.string.default_web_client_id)); + } + + public static boolean isFacebookMisconfigured(Context context) { + return AuthUI.UNCONFIGURED_CONFIG_VALUE.equals( + context.getString(R.string.facebook_application_id)); + } + + public static boolean isTwitterMisconfigured(Context context) { + List twitterConfigs = Arrays.asList( + context.getString(R.string.twitter_consumer_key), + context.getString(R.string.twitter_consumer_secret) + ); + + return twitterConfigs.contains(AuthUI.UNCONFIGURED_CONFIG_VALUE); + } + + public static boolean isGitHubMisconfigured(Context context) { + List gitHubConfigs = Arrays.asList( + context.getString(R.string.firebase_web_host), + context.getString(R.string.github_client_id), + context.getString(R.string.github_client_secret) + ); + + return gitHubConfigs.contains(AuthUI.UNCONFIGURED_CONFIG_VALUE); + } + + public static List getConfiguredProviders(Context context) { + List providers = new ArrayList<>(); + providers.add(new AuthUI.IdpConfig.EmailBuilder().build()); + providers.add(new AuthUI.IdpConfig.PhoneBuilder().build()); + + if (!isGoogleMisconfigured(context)) { + providers.add(new AuthUI.IdpConfig.GoogleBuilder().build()); + } + + if (!isFacebookMisconfigured(context)) { + providers.add(new AuthUI.IdpConfig.FacebookBuilder().build()); + } + + if (!isTwitterMisconfigured(context)) { + providers.add(new AuthUI.IdpConfig.TwitterBuilder().build()); + } + + if (!isGitHubMisconfigured(context)) { + providers.add(new AuthUI.IdpConfig.GitHubBuilder().build()); + } + + return providers; + } +} diff --git a/app/src/main/res/layout/activity_anonymous_upgrade.xml b/app/src/main/res/layout/activity_anonymous_upgrade.xml new file mode 100644 index 000000000..6ac9537e0 --- /dev/null +++ b/app/src/main/res/layout/activity_anonymous_upgrade.xml @@ -0,0 +1,67 @@ + + + + + + + +