From 7f944b9519b6875827a048864ee6ed2cf28e736b Mon Sep 17 00:00:00 2001 From: Renan Ferrari Date: Fri, 6 Nov 2020 14:18:24 -0300 Subject: [PATCH] Squashed 'libs/login/' changes from a3a1c83492..0f54aa634f 0f54aa634f Merge pull request #46 from wordpress-mobile/merge/wcandroid-ul-m1 69cdd9109b Fix merge conflicts a955de5657 Remove confusion over tag usage by renaming and fix social login bug ff2b2cbd3d Update to conform to changes from the Login Library ac21a9e49a Merge commit '7fb87d9b60e417020e48bc33b89f4a3ffeb88a95' into issue/merge-login-lib-changes c27418c7b3 Merge pull request #45 from wordpress-mobile/merge/WordPress-Android/latest-login-changes c616393331 Call login listener method for unregistered email if in WPcom login flow 895a4f59bd Merge pull request #2930 from woocommerce/release/5.1 b12126af34 Bump default LoginFlow's FluxC version fc65d5c9fb Merge pull request #2918 from woocommerce/issue/2911-tracks cb75217607 Merge pull request #2916 from woocommerce/issue/2897-overlap-ul 427d2e3980 Add new methods for updating the step when resuming site creds screen 37998c47b3 Add new methods for updating the step when resuming magic link and site creds views c4964cf8f5 Add new methods for updating the step when resuming site address and email password views e87a28cfa2 Nest layout in a ScrollView so buttons no longer overlap on smaller screens e58d87278c Make screen scrollable for smaller displays and lower resolutions 5cf377a253 Allow login to complete if WPcom login without the woo-specific site address check af41bb2b09 Null the site address view when the view is destroyed a8dae110be Merge pull request #12978 from wordpress-mobile/issue/12957-atomic-jetpack-site-address-login 41413fdc1c Merge pull request #12948 from wordpress-mobile/update-jdk-to-11 537a92ee4e Update string to remove reference to a missing button 8f88ebe3ab Preserve behavior when adding WP.com sites from add self-hosted option aea9679a58 Add internal reference comment related to Jetpack/Atomic detection logic 3f759c6ba9 Disable signup with Google when coming from Site Address flow 1680680257 Update Email screen label when logging in to a specific site address 83f9ed241b Update logic to handle connect site info for Jetpack and Atomic sites a61da09836 Merge branch 'develop' into issue/12832-private-custom-domain-2fa-wpcom-site 9aefabe4fd Resolved conflicts in login library build.gradle, pulled develop 091a99f0dc Updates robolectric to v4.4 in order to fix tenor tests 402cb0a936 Merge pull request #12896 from wordpress-mobile/oguz/upgrade-to-gradle-6-v2 c67f6fb7c6 Fix login flow memory leaks 6ae2e115b5 Use all distribution-type for gradlew b018cc6936 Extract method with logic to create connect site info event properties 2997608feb Add missing switch case on discovery error handling 695044dfac Merge pull request #2843 from woocommerce/issue/2721-tracks-2 f0302a7a81 Hide TOS buttons if not in signup mode during login 130b83f354 Updated gradle dependecy 3d99e1a63c Only log track failures if message is not null b74d879048 Upgrade Gradle to 6 c8aa9f43d3 Remove check for existing sites as it doesn't work with self-signed SSL 83a11da8eb Update site address screen to only end progress after discovery process 7d4e6202d6 Move convenience methods for controlling progress to login base class e6c9c0f75f Remove obsolete parameters siteName and siteIconUrl from Username screen 67522d2e0e Remove event handler for unused endpoint response 711479a2b0 Use connect/site-info endpoint for WordPress 723c471861 Extract method with logic to handle connect site info for Woo 77a58d99d0 Extract method with logic to determine if site has Jetpack 0d123fa463 Move tracking method to correct place bcf6c4e92e Add click tracking to magic link option on the Password screen a75cd04898 Update login magic link labels 5a21753625 Update signup magic link labels 50bd8fcd62 Merge branch 'develop' into feature/passwordless-flow f47f392f1e Made the backstack popping logic be applied to forced request instead of only passwordless. ee73ba4dbc Merge pull request #12871 from wordpress-mobile/issue/12730-avatar-helper-exception 11680d795c Removed another unused import. d1064580f2 Update Email screen to disable hint picker dialog if autofill is enabled 771c88d8d0 Enable autofill hints for email and password fields 07eeda436d Bump Google Play Services authentication library to 18.1.0 de44490864 Update model parameter to make it nullable a734985ff4 Update button labels and add click events c186654291 Update Email and Password screens to redirect passwordless users 946041bbb4 Add logic to route to login with wpcom creds flow 7244e26b70 Add magic link button to Email/Password screen 2cbadb4f9b Replace availability check with auth options 68141db621 Merge pull request #44 from wordpress-mobile/merge/WordPress-Android/12797 e4bbbb9f87 Merge pull request #12797 from wordpress-mobile/fix/uls-minor-tracking-issues c33cc926a8 Update Login2FaFragment to prevent tracking unneeded failure event 011aa2f5e6 Add tracking to password challenge step 45f625d367 Style site creds login by magic link verification screen f684c59340 Handle magic link login b4c8b8c192 Create alternative login email screen that provides site creds option 37e2e9c43f Merge pull request #43 from wordpress-mobile/merge/WordPress-Android/12727 2bcac63a70 Update Google sign-in to avoid tracking an UNKNOWN_USER error on signup 8d4ac008db Delete empty layouts created during merge and comment out usage c9ace9dc00 Merge commit 'b2b772d616c7d7e40c2b0d1c7d7a25e52ecbf59e' into issue/2655-unified-login-1 673ce5d366 Fix FluxC build 4504d28400 Merge remote-tracking branch 'origin/develop' into integrate/encrypted-logging 7a466b64e1 Merge pull request #42 from wordpress-mobile/merge/WordPress-Android/12538 69420f4718 Disable continue button when verification code is empty a651591cab Merge pull request #41 from wordpress-mobile/merge/uls-changes 4a0f98e7d0 Update signatures of LoginAnalyticsListener methods that receive properties 34c2a6725d Update LoginEmailFragment to avoid trying to disable nonexistent button db4a99ece6 Merge pull request #12503 from wordpress-mobile/add-source-to-account-created-event e703eb9ae1 Update trackCreatedAccount to use CreatedAccountSource 9dfa35975d Introduce CreatedAccountSource to LoginAnalyticsListener 2d4870d234 Convert LoginAnalyticsListener to Kotlin 5680616a17 Remove temporary GravatarUtils from AvatarHelper 4aaa4560e3 Update AvatarHelper to use fixed GravatarUtils f35f72f886 Bump Utils library version in the Login library 9fdad2487a Adds initial version of EncryptedLogging 92fb1820a0 Merge pull request #12415 from wordpress-mobile/feature/disable-primary-buttons-on-empty-fields 0dc932d182 Remove unnecessary non-null checks 28fa9f74f2 Merge branch 'develop' into feature/unified-login-signup 2549443fe7 Merge pull request #12010 from wordpress-mobile/merge/loginlib/woo-dark-mode 5014577306 Merge pull request #12425 from wordpress-mobile/issue/unified-login-signup-theming-issues 7ce7661f0d Stop using the default Gravatar placeholder on Login Epilogue b8b08aeff8 Update placeholder color to ensure compatibility on older API levels e486867240 Disable primary button when username or password are missing 1d3b66e9a6 Disable primary button when password is not filled in 1ace43e4ae Disable continue button when email is empty 581c9e97ec Update LoginFlow theme to ensure compatibility on older API levels b2971aab66 Remove nested selectors to ensure compatibility on older API levels 0844510f93 Merge pull request #12294 from wordpress-mobile/fix/remove-unused-click-event c970ac36bf Merge branch 'develop' into feature/unified-login-signup 17e0788dbe Remove event that's never triggered in the new flow f87d1e5448 Add missing click tracking to magic link screens f9ea9f696a Add click tracking to Signup Confirmation screen 23a0b2793c Add screen tracking to Signup Confirmation screen 497686214f Fix a bug when flow is switched when returning from the google flow 163646574c Merge branch 'feature/unified-login-failure-tracking' into feature/unified-login-interaction-tracking 18d01c6190 Move google error tracking to LoginActivity.java 6ce30e4a18 Merge branch 'feature/unified-login-signup' into feature/unified-login-failure-tracking c2ecfda3f2 Merge pull request #12056 from wordpress-mobile/issue/11785-add-signup-confirmation-screen effb35b47b Fix indentation issue cbc341d89c Add tracking for the email hints dialog 17cee3ef05 Add email click tracking d3c5f3f7ae Add click tracking to help f772966dde Merge branch 'feature/unified-login-failure-tracking' into feature/unified-login-interaction-tracking 6fbfcd7249 Add error logging where it was missing 7cadea49ed Cleanup failure tracking 646ba2ded7 Merge branch 'feature/unified-login-signup' into feature/unified-login-failure-tracking cbec230041 Merge pull request #12048 from wordpress-mobile/issue/11785-update-login-flow-layouts 546fb2b245 Implement help dialog on the signup confirmation screen fa0db85fba Implement signup confirmation screen for google signup 9cdcbc7af1 Implement signup confirmation screen for email signup dd7b6736f3 Update TOS links colors b42a8b13f6 Merge pull request #12047 from wordpress-mobile/issue/11785-update-login-flow-theme 855ac5269e Extract avatar loading logic to a helper class 30f1fadf17 Update email header component layout 7bdca2d2a0 Update Signup Magic Link screen 7ca256af24 Rename Signup Magic Link screen layout file 06a2d4c6e5 Update Signup Email screen fcd4a97d2a Rename Signup Email screen layout file 0048078ec0 Update Login Username Password screen 21b3fbb250 Update Login Site Address screen 9a453c4329 Update Login 2FA screen 1c2610f396 Update Login Email Password screen a06c6bd863 Update Login Magic Link Sent screen 951456aaaa Extract magic link header to a separate layout file 1a4100633d UI update to Magic Link Request screen 1c2526d13b Update base form layout 668da0a21d Extract old Login Email screen to a separate layout file 5e942b4176 UI update to Login Email screen 80a4c0d908 Update login theme and styles 16dac21f5e Merge pull request #11944 from wordpress-mobile/issue/11783-update-epilogue-ui a06967559d Update gradle plugin to 4.0 and gradle to 6.1.1 b3d8998027 Merge commit '46923a963c480113b797856bfaf179dad5044d2d' into merge/loginlib/woo-dark-mode 4f1af83e96 Fix NPE when primary button is missing d0cfd5ed63 End progress when the login fragment is destroyed 2274f87e22 Add click tracking to unified login and signup flow 81c9feeb28 Use universal method for failure tracking 9e5f139de1 First step of failure tracking 9cebf7a88b Remove method that's not tracking screen change c6469d5b9e Add source to tracking events eefd096bcd Add screen tracking to login and signup fdc65a5240 Bump login library minSdkVersion to 21 and targetSdkVersion to 29 44e989e866 Merge pull request #12000 from wordpress-mobile/fix/9905_Crash_InflateException_Binary_XML_file_line_NEW eac3784277 Revert unintentional change during merge 5502b855c4 Merge commit 'd4d2d895dd0c1469adf230607fe801146f9f91ff' into merge/loginlib/woo-dark-mode 1f1f28318c Merge pull request #11934 from wordpress-mobile/update/fragments 0c40f732f4 Set password icon programmatically eb8ed2635f Fix wrong password icon f0e7106f68 Remove unused resources 044893cfb7 Update login epilogue secondary button strings c5c79810c9 Update signup epilogue primary button string fb1712af70 fixed lint error and replaced all observers owner parameter with viewLifecycleOwner 2fb113e0d8 Revert changes to gradle a57069b24b Gradle updates. 05af8f4c85 Remove unused strings from the login library 0f40e7e9e6 Add ability to enable login via site address from the prologue screen 689b1d9574 Add ability to disable login via site address from the email screen fd236a31e6 Merge pull request #11717 from wordpress-mobile/issue/11705-google-sign-in 92ef161140 Move @Override annotation to conform with the current code style 8871b22436 Merge branch 'feature/unified-login-signup' into feature/introduce-kotlin debde7c28e Add library version locally to the login library e7e8df0379 Move progress dialog to better handle configuration changes 1c871c9f33 Update log messages for sign-up from login functionality 9c13cb0cf3 Add flag to turn the sign-up from login functionality on 3cae24c922 Add loading dialog to LoginGoogleFragment a00ec92d95 Update LoginGoogleFragment so it supports sign-up from login a09bdda7c5 Use correct versions to fix build 217b13ff0b Introduce kotlin to LoginFlow and bump libraries 79c4db7291 Add flag to turn the sign-up from login functionality on beff169fc2 Update LoginEmailFragment so it supports sign-up from login git-subtree-dir: libs/login git-subtree-split: 0f54aa634f76df6223d4eabd4bf37c01a898d1ec --- WordPressLoginFlow/build.gradle | 52 ++- .../wordpress/android/login/AuthOptions.kt | 6 + .../android/login/GoogleFragment.java | 14 +- .../android/login/Login2FaFragment.java | 47 +- .../android/login/LoginAnalyticsListener.java | 54 --- .../android/login/LoginAnalyticsListener.kt | 85 ++++ .../login/LoginBaseDiscoveryFragment.java | 1 + .../android/login/LoginBaseFormFragment.java | 59 ++- .../android/login/LoginEmailFragment.java | 409 +++++++++++++++--- .../login/LoginEmailPasswordFragment.java | 93 +++- .../android/login/LoginGoogleFragment.java | 40 +- .../android/login/LoginListener.java | 17 +- .../login/LoginMagicLinkRequestFragment.java | 135 +++--- .../login/LoginMagicLinkSentFragment.java | 41 +- .../login/LoginSiteAddressFragment.java | 220 ++++++---- .../LoginSiteAddressHelpDialogFragment.java | 1 + .../login/LoginUsernamePasswordFragment.java | 105 +++-- .../login/SignupConfirmationFragment.kt | 172 ++++++++ .../android/login/SignupEmailFragment.java | 31 +- .../android/login/SignupGoogleFragment.java | 69 ++- .../login/SignupMagicLinkFragment.java | 8 +- .../android/login/di/LoginFragmentModule.java | 4 + .../android/login/util/AvatarHelper.kt | 67 +++ .../android/login/util/ContextExtensions.kt | 29 ++ .../login/widgets/WPLoginInputRow.java | 7 + .../login_on_background_medium_selector.xml | 5 + .../color/login_on_surface_high_selector.xml | 5 + .../login_on_surface_medium_selector.xml | 5 + .../material_on_surface_emphasis_low.xml | 4 + .../drawable/ic_help_outline_white_24dp.xml | 11 + .../ic_user_circle_no_padding_grey_24dp.xml | 9 + .../login_magic_link_request_screen.xml | 86 ---- .../login_magic_link_sent_screen.xml | 77 ---- .../signup_bottom_sheet_dialog.xml | 6 +- .../res/layout-land/signup_magic_link.xml | 68 --- .../src/main/res/layout/login_2fa_screen.xml | 20 +- .../main/res/layout/login_alert_http_auth.xml | 4 +- .../layout/login_alert_site_address_help.xml | 2 +- ...login_email_optional_site_creds_screen.xml | 87 ++++ .../layout/login_email_password_screen.xml | 97 ++--- .../main/res/layout/login_email_screen.xml | 212 +++++---- .../res/layout/login_email_screen_old.xml | 105 +++++ .../src/main/res/layout/login_form_screen.xml | 50 +-- .../res/layout/login_include_email_header.xml | 37 ++ .../src/main/res/layout/login_input_row.xml | 2 +- .../login_magic_link_request_screen.xml | 82 ++-- .../layout/login_magic_link_sent_screen.xml | 82 ++-- .../res/layout/login_site_address_screen.xml | 23 +- .../layout/login_username_password_screen.xml | 109 +---- .../res/layout/signup_bottom_sheet_dialog.xml | 6 +- .../res/layout/signup_confirmation_screen.xml | 51 +++ ...l_fragment.xml => signup_email_screen.xml} | 12 +- .../src/main/res/layout/signup_magic_link.xml | 50 --- .../res/layout/signup_magic_link_screen.xml | 49 +++ .../src/main/res/layout/toolbar_login.xml | 7 +- .../src/main/res/menu/menu_login.xml | 8 +- .../src/main/res/values-night/colors.xml | 6 + .../src/main/res/values-night/themes.xml | 27 ++ .../src/main/res/values-v23/themes.xml | 8 + .../src/main/res/values-v27/styles.xml | 10 - .../src/main/res/values-v27/themes.xml | 10 + .../src/main/res/values/colors.xml | 20 +- .../src/main/res/values/dimens.xml | 4 +- .../src/main/res/values/shapes.xml | 11 + .../src/main/res/values/strings.xml | 47 +- .../src/main/res/values/styles.xml | 141 +++--- .../src/main/res/values/themes.xml | 62 +++ .../src/main/res/values/types.xml | 70 +++ gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 58695 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 109 +++-- gradlew.bat | 33 +- 72 files changed, 2474 insertions(+), 1224 deletions(-) create mode 100644 WordPressLoginFlow/src/main/java/org/wordpress/android/login/AuthOptions.kt delete mode 100644 WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginAnalyticsListener.java create mode 100644 WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginAnalyticsListener.kt create mode 100644 WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupConfirmationFragment.kt create mode 100644 WordPressLoginFlow/src/main/java/org/wordpress/android/login/util/AvatarHelper.kt create mode 100644 WordPressLoginFlow/src/main/java/org/wordpress/android/login/util/ContextExtensions.kt create mode 100644 WordPressLoginFlow/src/main/res/color/login_on_background_medium_selector.xml create mode 100644 WordPressLoginFlow/src/main/res/color/login_on_surface_high_selector.xml create mode 100644 WordPressLoginFlow/src/main/res/color/login_on_surface_medium_selector.xml create mode 100644 WordPressLoginFlow/src/main/res/color/material_on_surface_emphasis_low.xml create mode 100644 WordPressLoginFlow/src/main/res/drawable/ic_help_outline_white_24dp.xml create mode 100644 WordPressLoginFlow/src/main/res/drawable/ic_user_circle_no_padding_grey_24dp.xml delete mode 100644 WordPressLoginFlow/src/main/res/layout-land/login_magic_link_request_screen.xml delete mode 100644 WordPressLoginFlow/src/main/res/layout-land/login_magic_link_sent_screen.xml delete mode 100644 WordPressLoginFlow/src/main/res/layout-land/signup_magic_link.xml create mode 100644 WordPressLoginFlow/src/main/res/layout/login_email_optional_site_creds_screen.xml create mode 100644 WordPressLoginFlow/src/main/res/layout/login_email_screen_old.xml create mode 100644 WordPressLoginFlow/src/main/res/layout/login_include_email_header.xml create mode 100644 WordPressLoginFlow/src/main/res/layout/signup_confirmation_screen.xml rename WordPressLoginFlow/src/main/res/layout/{signup_email_fragment.xml => signup_email_screen.xml} (71%) delete mode 100644 WordPressLoginFlow/src/main/res/layout/signup_magic_link.xml create mode 100644 WordPressLoginFlow/src/main/res/layout/signup_magic_link_screen.xml create mode 100644 WordPressLoginFlow/src/main/res/values-night/colors.xml create mode 100644 WordPressLoginFlow/src/main/res/values-night/themes.xml create mode 100644 WordPressLoginFlow/src/main/res/values-v23/themes.xml delete mode 100644 WordPressLoginFlow/src/main/res/values-v27/styles.xml create mode 100644 WordPressLoginFlow/src/main/res/values-v27/themes.xml create mode 100644 WordPressLoginFlow/src/main/res/values/shapes.xml create mode 100644 WordPressLoginFlow/src/main/res/values/themes.xml create mode 100644 WordPressLoginFlow/src/main/res/values/types.xml diff --git a/WordPressLoginFlow/build.gradle b/WordPressLoginFlow/build.gradle index 9051314a8edb..be03ca577539 100644 --- a/WordPressLoginFlow/build.gradle +++ b/WordPressLoginFlow/build.gradle @@ -1,28 +1,38 @@ buildscript { + ext { + kotlin_version = '1.3.61' + kotlin_ktx_version = '1.2.0' + daggerVersion = '2.22.1' + appCompatVersion = '1.0.2' + } repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.3' + classpath 'com.android.tools.build:gradle:4.0.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-android-extensions' repositories { google() jcenter() maven { url "https://www.jitpack.io" } + maven { url "http://dl.bintray.com/terl/lazysodium-maven" } } android { - compileSdkVersion 28 - buildToolsVersion "28.0.3" + compileSdkVersion 29 defaultConfig { - minSdkVersion 17 - targetSdkVersion 28 + minSdkVersion 21 + targetSdkVersion 29 versionCode 2 versionName "1.1" @@ -31,18 +41,22 @@ android { } dependencies { - implementation ('org.wordpress:utils:1.20.3') { + implementation ('org.wordpress:utils:1.26') { exclude group: "com.android.volley" } - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.vectordrawable:vectordrawable-animated:1.0.0' - implementation 'androidx.media:media:1.0.1' + implementation "androidx.appcompat:appcompat:$appCompatVersion" + implementation 'androidx.vectordrawable:vectordrawable-animated:1.1.0' + implementation 'androidx.media:media:1.1.0' implementation 'androidx.legacy:legacy-support-v13:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'com.google.android.material:material:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + + implementation "androidx.core:core-ktx:$kotlin_ktx_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - api 'com.google.android.gms:play-services-auth:15.0.1' + api 'com.google.android.gms:play-services-auth:18.1.0' // Share FluxC version from host project if defined, otherwise fallback to default if (project.hasProperty("fluxCVersion")) { @@ -51,28 +65,28 @@ dependencies { exclude group: "org.wordpress", module: "utils" } } else { - implementation("com.github.wordpress-mobile.WordPress-FluxC-Android:fluxc:1.5.1-beta-4") { + implementation("com.github.wordpress-mobile.WordPress-FluxC-Android:fluxc:1.6.22") { exclude group: "com.android.support" exclude group: "org.wordpress", module: "utils" } } implementation 'com.github.bumptech.glide:glide:4.10.0' - annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0' + kapt 'com.github.bumptech.glide:compiler:4.10.0' // Dagger - implementation 'com.google.dagger:dagger:2.22.1' - annotationProcessor 'com.google.dagger:dagger-compiler:2.22.1' + implementation "com.google.dagger:dagger:$daggerVersion" + kapt "com.google.dagger:dagger-compiler:$daggerVersion" compileOnly 'org.glassfish:javax.annotation:10.0-b28' - implementation 'com.google.dagger:dagger-android-support:2.22.1' - annotationProcessor 'com.google.dagger:dagger-android-processor:2.22.1' + implementation "com.google.dagger:dagger-android-support:$daggerVersion" + kapt "com.google.dagger:dagger-android-processor:$daggerVersion" lintChecks 'org.wordpress:lint:1.0.1' testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.27.0' - testImplementation 'androidx.arch.core:core-testing:2.0.1' - testImplementation 'org.robolectric:robolectric:4.3' + testImplementation 'org.mockito:mockito-core:2.28.2' + testImplementation 'androidx.arch.core:core-testing:2.1.0' + testImplementation 'org.robolectric:robolectric:4.4' testImplementation 'org.assertj:assertj-core:3.11.1' } diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/AuthOptions.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/AuthOptions.kt new file mode 100644 index 000000000000..2aa545f0a882 --- /dev/null +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/AuthOptions.kt @@ -0,0 +1,6 @@ +package org.wordpress.android.login + +data class AuthOptions( + val isPasswordless: Boolean, + val isEmailVerified: Boolean +) diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/GoogleFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/GoogleFragment.java index 1d41c8d45593..33a9ad617e4d 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/GoogleFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/GoogleFragment.java @@ -22,6 +22,9 @@ import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import javax.inject.Inject; import static android.app.Activity.RESULT_OK; @@ -52,7 +55,7 @@ public class GoogleFragment extends Fragment implements ConnectionCallbacks, OnC protected String mIdToken; protected String mPhotoUrl; - protected static final String SERVICE_TYPE_GOOGLE = "google"; + public static final String SERVICE_TYPE_GOOGLE = "google"; @Inject protected Dispatcher mDispatcher; @Inject protected SiteStore mSiteStore; @@ -246,4 +249,13 @@ public void onActivityResult(int request, int result, Intent data) { break; } } + + // Remove scale from photo URL path string. Current URL matches /s96-c, which returns a 96 x 96 + // pixel image. Removing /s96-c from the string returns a 512 x 512 pixel image. Using regular + // expressions may help if the photo URL scale value in the returned path changes. + protected String removeScaleFromGooglePhotoUrl(String photoUrl) { + Pattern pattern = Pattern.compile("(/s[0-9]+-c)"); + Matcher matcher = pattern.matcher(photoUrl); + return matcher.find() ? photoUrl.replace(matcher.group(1), "") : photoUrl; + } } diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java index 96b2baab5a15..c638379afc2b 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/Login2FaFragment.java @@ -18,7 +18,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; +import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.Toolbar; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -89,7 +91,7 @@ public class Login2FaFragment extends LoginBaseFormFragment imple ArrayList mOldSitesIDs; - private Button mSecondaryButton; + private Button mOtpButton; private String mEmailAddress; private String mIdToken; private String mNonce; @@ -172,22 +174,23 @@ protected void setupContent(ViewGroup rootView) { // restrict the allowed input chars to just numbers m2FaInput.getEditText().setKeyListener(DigitsKeyListener.getInstance("0123456789")); - } - @Override - protected void setupBottomButtons(Button secondaryButton, Button primaryButton) { - secondaryButton.setText(R.string.login_text_otp); - secondaryButton.setOnClickListener(new OnClickListener() { + mOtpButton = rootView.findViewById(R.id.login_otp_button); + mOtpButton.setText(mSentSmsCode ? R.string.login_text_otp_another : R.string.login_text_otp); + mOtpButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (isAdded()) { + mAnalyticsListener.trackSendCodeWithTextClicked(); doAuthAction(R.string.requesting_otp, "", true); } } }); - secondaryButton.setText(getString(mSentSmsCode ? R.string.login_text_otp_another : R.string.login_text_otp)); - mSecondaryButton = secondaryButton; + } + @Override + protected void setupBottomButtons(Button secondaryButton, Button primaryButton) { + secondaryButton.setVisibility(View.GONE); primaryButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { next(); @@ -195,6 +198,11 @@ public void onClick(View v) { }); } + @Override + protected void buildToolbar(Toolbar toolbar, ActionBar actionBar) { + actionBar.setTitle(R.string.log_in); + } + @Override protected EditText getEditTextToFocusOnStart() { return m2FaInput.getEditText(); @@ -275,9 +283,12 @@ public void onResume() { if (TextUtils.isEmpty(m2FaInput.getEditText().getText())) { m2FaInput.setText(getAuthCodeFromClipboard()); } + + updateContinueButtonEnabledStatus(); } protected void next() { + mAnalyticsListener.trackSubmit2faCodeClicked(); if (TextUtils.isEmpty(m2FaInput.getEditText().getText())) { show2FaError(getString(R.string.login_empty_2fa)); return; @@ -318,7 +329,7 @@ private String getAuthCodeFromClipboard() { ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(CLIPBOARD_SERVICE); if (clipboard.getPrimaryClip() != null && clipboard.getPrimaryClip().getItemAt(0) != null - && clipboard.getPrimaryClip().getItemAt(0).getText() != null) { + && clipboard.getPrimaryClip().getItemAt(0).getText() != null) { String code = clipboard.getPrimaryClip().getItemAt(0).getText().toString(); final Matcher twoStepAuthCodeMatcher = TWO_STEP_AUTH_CODE.matcher(""); @@ -365,12 +376,21 @@ public void beforeTextChanged(CharSequence s, int start, int count, int after) { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { show2FaError(null); + updateContinueButtonEnabledStatus(); } - private void show2FaError(String message) { + private void show2FaError(@Nullable String message) { + if (!TextUtils.isEmpty(message)) { + mAnalyticsListener.trackFailure(message); + } m2FaInput.setError(message); } + private void updateContinueButtonEnabledStatus() { + String currentVerificationCode = m2FaInput.getEditText().getText().toString(); + getPrimaryButton().setEnabled(!currentVerificationCode.trim().isEmpty()); + } + @Override protected void endProgress() { super.endProgress(); @@ -389,7 +409,7 @@ private void handleAuthError(AuthenticationErrorType error, String errorMessage) // TODO: FluxC: could be specific? default: AppLog.e(T.NUX, "Server response: " + errorMessage); - + mAnalyticsListener.trackFailure(errorMessage); ToastUtils.showToast(getActivity(), errorMessage == null ? getString(R.string.error_generic) : errorMessage); break; @@ -397,6 +417,7 @@ private void handleAuthError(AuthenticationErrorType error, String errorMessage) } private void showErrorDialog(String message) { + mAnalyticsListener.trackFailure(message); AlertDialog dialog = new MaterialAlertDialogBuilder(getActivity()) .setMessage(message) .setPositiveButton(R.string.login_error_button, null) @@ -485,7 +506,7 @@ public void onSocialChanged(OnSocialChanged event) { mAnalyticsListener.trackSocialConnectFailure(); doFinishLogin(); } - // Two-factor authentication code was sent via SMS to account phone number; replace SMS nonce with response. + // Two-factor authentication code was sent via SMS to account phone number; replace SMS nonce with response. } else if (!TextUtils.isEmpty(event.phoneNumber) && !TextUtils.isEmpty(event.nonce)) { endProgress(); mPhoneNumber = event.phoneNumber; @@ -514,7 +535,7 @@ protected void onLoginFinished() { private void setTextForSms() { mLabel.setText(getString(R.string.enter_verification_code_sms, mPhoneNumber)); - mSecondaryButton.setText(getString(R.string.login_text_otp_another)); + mOtpButton.setText(getString(R.string.login_text_otp_another)); mSentSmsCode = true; } } diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginAnalyticsListener.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginAnalyticsListener.java deleted file mode 100644 index e79ad1202db1..000000000000 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginAnalyticsListener.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.wordpress.android.login; - -import java.util.Map; - -public interface LoginAnalyticsListener { - void trackAnalyticsSignIn(boolean isWpcomLogin); - void trackCreatedAccount(String username, String email); - void trackEmailFormViewed(); - void trackInsertedInvalidUrl(); - void trackLoginAccessed(); - void trackLoginAutofillCredentialsFilled(); - void trackLoginAutofillCredentialsUpdated(); - void trackLoginFailed(String errorContext, String errorType, String errorDescription); - void trackLoginForgotPasswordClicked(); - void trackLoginMagicLinkExited(); - void trackLoginMagicLinkOpened(); - void trackLoginMagicLinkOpenEmailClientClicked(); - void trackLoginMagicLinkSucceeded(); - void trackLoginSocial2faNeeded(); - void trackLoginSocialSuccess(); - void trackMagicLinkFailed(Map properties); - void trackMagicLinkOpenEmailClientViewed(); - void trackMagicLinkRequested(); - void trackMagicLinkRequestFormViewed(); - void trackPasswordFormViewed(); - void trackSignupCanceled(); - void trackSignupEmailButtonTapped(); - void trackSignupEmailToLogin(); - void trackSignupGoogleButtonTapped(); - void trackSignupMagicLinkFailed(); - void trackSignupMagicLinkOpened(); - void trackSignupMagicLinkOpenEmailClientClicked(); - void trackSignupMagicLinkSent(); - void trackSignupMagicLinkSucceeded(); - void trackSignupSocialAccountsNeedConnecting(); - void trackSignupSocialButtonFailure(); - void trackSignupSocialToLogin(); - void trackSignupTermsOfServiceTapped(); - void trackSocialAccountsNeedConnecting(); - void trackSocialButtonClick(); - void trackSocialButtonFailure(); - void trackSocialConnectFailure(); - void trackSocialConnectSuccess(); - void trackSocialErrorUnknownUser(); - void trackSocialFailure(String errorContext, String errorType, String errorDescription); - void trackTwoFactorFormViewed(); - void trackUrlFormViewed(); - void trackUrlHelpScreenViewed(); - void trackUsernamePasswordFormViewed(); - void trackWpComBackgroundServiceUpdate(Map properties); - void trackConnectedSiteInfoRequested(String url); - void trackConnectedSiteInfoFailed(String url, String errorContext, String errorType, String errorDescription); - void trackConnectedSiteInfoSucceeded(Map properties); -} diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginAnalyticsListener.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginAnalyticsListener.kt new file mode 100644 index 000000000000..379923d31946 --- /dev/null +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginAnalyticsListener.kt @@ -0,0 +1,85 @@ +package org.wordpress.android.login + +import java.util.Locale + +interface LoginAnalyticsListener { + fun trackAnalyticsSignIn(isWpcomLogin: Boolean) + fun trackCreatedAccount(username: String?, email: String?, source: CreatedAccountSource) + fun trackEmailFormViewed() + fun trackInsertedInvalidUrl() + fun trackLoginAccessed() + fun trackLoginAutofillCredentialsFilled() + fun trackLoginAutofillCredentialsUpdated() + fun trackLoginFailed(errorContext: String?, errorType: String?, errorDescription: String?) + fun trackLoginForgotPasswordClicked() + fun trackLoginMagicLinkExited() + fun trackLoginMagicLinkOpened() + fun trackLoginMagicLinkOpenEmailClientClicked() + fun trackLoginMagicLinkSucceeded() + fun trackLoginSocial2faNeeded() + fun trackLoginSocialSuccess() + fun trackMagicLinkFailed(properties: Map) + fun trackSignupMagicLinkOpenEmailClientViewed() + fun trackLoginMagicLinkOpenEmailClientViewed() + fun trackMagicLinkRequested() + fun trackMagicLinkRequestFormViewed() + fun trackPasswordFormViewed(isSocialChallenge: Boolean) + fun trackSignupCanceled() + fun trackSignupEmailButtonTapped() + fun trackSignupEmailToLogin() + fun trackSignupGoogleButtonTapped() + fun trackSignupMagicLinkFailed() + fun trackSignupMagicLinkOpened() + fun trackSignupMagicLinkOpenEmailClientClicked() + fun trackSignupMagicLinkSent() + fun trackSignupMagicLinkSucceeded() + fun trackSignupSocialAccountsNeedConnecting() + fun trackSignupSocialButtonFailure() + fun trackSignupSocialToLogin() + fun trackSignupTermsOfServiceTapped() + fun trackSocialButtonStart() + fun trackSocialAccountsNeedConnecting() + fun trackSocialButtonClick() + fun trackSocialButtonFailure() + fun trackSocialConnectFailure() + fun trackSocialConnectSuccess() + fun trackSocialErrorUnknownUser() + fun trackSocialFailure(errorContext: String?, errorType: String?, errorDescription: String?) + fun trackTwoFactorFormViewed() + fun trackUrlFormViewed() + fun trackUrlHelpScreenViewed() + fun trackUsernamePasswordFormViewed() + fun trackWpComBackgroundServiceUpdate(properties: Map) + fun trackConnectedSiteInfoRequested(url: String?) + fun trackConnectedSiteInfoFailed(url: String?, errorContext: String?, errorType: String?, errorDescription: String?) + fun trackConnectedSiteInfoSucceeded(properties: Map) + fun trackFailure(message: String?) + fun trackSendCodeWithTextClicked() + fun trackSubmit2faCodeClicked() + fun trackSubmitClicked() + fun trackRequestMagicLinkClick() + fun trackLoginWithPasswordClick() + fun trackShowHelpClick() + fun trackDismissDialog() + fun trackSelectEmailField() + fun trackPickEmailFromHint() + fun trackShowEmailHints() + fun emailFormScreenResumed() + fun trackEmailSignupConfirmationViewed() + fun trackSocialSignupConfirmationViewed() + fun trackCreateAccountClick() + fun emailPasswordFormScreenResumed() + fun siteAddressFormScreenResumed() + fun magicLinkRequestScreenResumed() + fun magicLinkSentScreenResumed() + fun usernamePasswordScreenResumed() + + enum class CreatedAccountSource { + EMAIL, + GOOGLE; + + fun asPropertyMap() = hashMapOf( + "source" to name.toLowerCase(Locale.ROOT) + ) + } +} diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginBaseDiscoveryFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginBaseDiscoveryFragment.java index 4390ab57d394..40c916f6ee1c 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginBaseDiscoveryFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginBaseDiscoveryFragment.java @@ -72,6 +72,7 @@ public void onDiscoverySucceeded(OnDiscoveryResponse event) { } private void handleDiscoveryError(DiscoveryError error, final String failedEndpoint) { + mAnalyticsListener.trackFailure(error.name() + " - " + failedEndpoint); if (error == DiscoveryError.WORDPRESS_COM_SITE) { mLoginBaseDiscoveryListener.handleWpComDiscoveryError(failedEndpoint); } else { diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginBaseFormFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginBaseFormFragment.java index 09d1e009e6ff..f48c32057ef9 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginBaseFormFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginBaseFormFragment.java @@ -12,6 +12,7 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; import android.view.ViewStub; import android.widget.Button; import android.widget.EditText; @@ -116,6 +117,15 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mSecondaryButton = (Button) rootView.findViewById(R.id.secondary_button); setupBottomButtons(mSecondaryButton, mPrimaryButton); + // Set the primary button width to match_parent if the secondary button doesn't exist or isn't visible. + // This can be removed after we get rid of the unified flow feature flag. + if ((mSecondaryButton == null || mSecondaryButton.getVisibility() == View.GONE) + && (mPrimaryButton != null && mPrimaryButton.getVisibility() == View.VISIBLE)) { + final LayoutParams layoutParams = mPrimaryButton.getLayoutParams(); + layoutParams.width = LayoutParams.MATCH_PARENT; + mPrimaryButton.setLayoutParams(layoutParams); + } + return rootView; } @@ -128,8 +138,8 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); if (actionBar != null) { - actionBar.setDisplayShowTitleEnabled(false); actionBar.setDisplayHomeAsUpEnabled(true); + buildToolbar(toolbar, actionBar); } if (savedInstanceState == null) { @@ -137,6 +147,14 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { } } + protected void buildToolbar(Toolbar toolbar, ActionBar actionBar) { + View toolbarIcon = toolbar.findViewById(R.id.toolbar_icon); + if (toolbarIcon != null) { + toolbarIcon.setVisibility(View.VISIBLE); + } + actionBar.setDisplayShowTitleEnabled(false); + } + @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -200,6 +218,7 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.help) { + mAnalyticsListener.trackShowHelpClick(); onHelp(); return true; } @@ -207,6 +226,29 @@ public boolean onOptionsItemSelected(MenuItem item) { return false; } + @Override + public void onDestroy() { + endProgress(); + super.onDestroy(); + } + + @Override public void onDestroyView() { + mPrimaryButton = null; + mSecondaryButton = null; + + if (mProgressDialog != null) { + mProgressDialog.setOnCancelListener(null); + mProgressDialog = null; + } + super.onDestroyView(); + } + + protected void startProgressIfNeeded() { + if (!isInProgress()) { + startProgress(); + } + } + protected void startProgress() { startProgress(true); } @@ -223,14 +265,18 @@ protected void startProgress(boolean cancellable) { new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialogInterface) { - if (isInProgress()) { - endProgress(); - } + endProgressIfNeeded(); } }); mInProgress = true; } + protected void endProgressIfNeeded() { + if (isInProgress()) { + endProgress(); + } + } + @CallSuper protected void endProgress() { mInProgress = false; @@ -240,8 +286,9 @@ protected void endProgress() { mProgressDialog.setOnCancelListener(null); mProgressDialog = null; } - - mPrimaryButton.setEnabled(true); + if (mPrimaryButton != null) { + mPrimaryButton.setEnabled(true); + } if (mSecondaryButton != null) { mSecondaryButton.setEnabled(true); diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginEmailFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginEmailFragment.java index 5cd2f9b675ec..4f6b9ff9b25a 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginEmailFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginEmailFragment.java @@ -5,14 +5,18 @@ import android.content.Context; import android.content.Intent; import android.content.IntentSender; +import android.os.Build; import android.os.Bundle; import android.text.Editable; import android.text.Html; +import android.text.Spanned; +import android.text.TextUtils; import android.text.TextWatcher; import android.util.Patterns; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.view.autofill.AutofillManager; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; @@ -21,7 +25,9 @@ import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.Toolbar; import com.google.android.gms.auth.api.Auth; import com.google.android.gms.auth.api.credentials.Credential; @@ -37,8 +43,10 @@ import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.wordpress.android.fluxc.generated.AccountActionBuilder; -import org.wordpress.android.fluxc.store.AccountStore; -import org.wordpress.android.fluxc.store.AccountStore.OnAvailabilityChecked; +import org.wordpress.android.fluxc.store.AccountStore.FetchAuthOptionsPayload; +import org.wordpress.android.fluxc.store.AccountStore.OnAuthOptionsFetched; +import org.wordpress.android.login.SignupBottomSheetDialogFragment.SignupSheetListener; +import org.wordpress.android.login.util.ContextExtensionsKt; import org.wordpress.android.login.util.SiteUtils; import org.wordpress.android.login.widgets.WPLoginInputRow; import org.wordpress.android.login.widgets.WPLoginInputRow.OnEditorCommitListener; @@ -46,7 +54,10 @@ import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.EditTextUtils; +import org.wordpress.android.util.HtmlUtils; import org.wordpress.android.util.NetworkUtils; +import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.ToastUtils.Duration; import java.util.ArrayList; import java.util.regex.Matcher; @@ -70,8 +81,14 @@ public class LoginEmailFragment extends LoginBaseFormFragment imp private static final int EMAIL_CREDENTIALS_REQUEST_CODE = 25100; private static final String ARG_LOGIN_SITE_URL = "ARG_LOGIN_SITE_URL"; + private static final String ARG_SIGNUP_FROM_LOGIN_ENABLED = "ARG_SIGNUP_FROM_LOGIN_ENABLED"; + private static final String ARG_SITE_LOGIN_ENABLED = "ARG_SITE_LOGIN_ENABLED"; + private static final String ARG_SHOULD_USE_NEW_LAYOUT = "ARG_SHOULD_USE_NEW_LAYOUT"; + private static final String ARG_OPTIONAL_SITE_CREDS_LAYOUT = "ARG_OPTIONAL_SITE_CREDS_LAYOUT"; + private static final String ARG_HIDE_TOS = "ARG_HIDE_TOS"; public static final String TAG = "login_email_fragment_tag"; + public static final String TAG_SITE_CREDS_LAYOUT = "login_email_fragment_site_creds_layout_tag"; public static final int MAX_EMAIL_LENGTH = 100; private ArrayList mOldSitesIDs = new ArrayList<>(); @@ -80,6 +97,11 @@ public class LoginEmailFragment extends LoginBaseFormFragment imp private String mRequestedEmail; private boolean mIsSocialLogin; private Integer mCurrentEmailErrorRes = null; + private boolean mIsSignupFromLoginEnabled; + private boolean mIsSiteLoginEnabled; + private boolean mShouldUseNewLayout; + private boolean mOptionalSiteCredsLayout; + private boolean mHideTos; protected WPLoginInputRow mEmailInput; protected boolean mHasDismissedEmailHints; @@ -94,9 +116,61 @@ public static LoginEmailFragment newInstance(String url) { return fragment; } + public static LoginEmailFragment newInstance(String url, boolean optionalSiteCredsLayout) { + LoginEmailFragment fragment = new LoginEmailFragment(); + Bundle args = new Bundle(); + args.putString(ARG_LOGIN_SITE_URL, url); + args.putBoolean(ARG_OPTIONAL_SITE_CREDS_LAYOUT, optionalSiteCredsLayout); + fragment.setArguments(args); + return fragment; + } + + public static LoginEmailFragment newInstance(boolean isSignupFromLoginEnabled, + boolean isSiteLoginEnabled, + boolean shouldUseNewLayout) { + return newInstance( + isSignupFromLoginEnabled, isSiteLoginEnabled, shouldUseNewLayout, null); + } + + public static LoginEmailFragment newInstance(boolean isSignupFromLoginEnabled, + boolean isSiteLoginEnabled, + boolean shouldUseNewLayout, + boolean hideTos) { + LoginEmailFragment fragment = new LoginEmailFragment(); + Bundle args = new Bundle(); + args.putBoolean(ARG_SIGNUP_FROM_LOGIN_ENABLED, isSignupFromLoginEnabled); + args.putBoolean(ARG_SITE_LOGIN_ENABLED, isSiteLoginEnabled); + args.putBoolean(ARG_SHOULD_USE_NEW_LAYOUT, shouldUseNewLayout); + args.putBoolean(ARG_HIDE_TOS, hideTos); + args.putString(ARG_LOGIN_SITE_URL, null); + fragment.setArguments(args); + return fragment; + } + + public static LoginEmailFragment newInstance(boolean isSignupFromLoginEnabled, + boolean isSiteLoginEnabled, + boolean shouldUseNewLayout, + String url) { + LoginEmailFragment fragment = new LoginEmailFragment(); + Bundle args = new Bundle(); + args.putBoolean(ARG_SIGNUP_FROM_LOGIN_ENABLED, isSignupFromLoginEnabled); + args.putBoolean(ARG_SITE_LOGIN_ENABLED, isSiteLoginEnabled); + args.putBoolean(ARG_SHOULD_USE_NEW_LAYOUT, shouldUseNewLayout); + args.putBoolean(ARG_HIDE_TOS, false); + args.putString(ARG_LOGIN_SITE_URL, url); + fragment.setArguments(args); + return fragment; + } + @Override protected @LayoutRes int getContentLayout() { - return R.layout.login_email_screen; + if (mShouldUseNewLayout) { + return R.layout.login_email_screen; + } else if (mOptionalSiteCredsLayout) { + return R.layout.login_email_optional_site_creds_screen; + } else { + return R.layout.login_email_screen_old; + } } @Override @@ -115,10 +189,21 @@ protected void setupLabel(@NonNull TextView label) { break; case FULL: case WPCOM_LOGIN_ONLY: - label.setText(R.string.enter_email_wordpress_com); + case SELFHOSTED_ONLY: + if (!mShouldUseNewLayout) { + label.setText(R.string.enter_email_wordpress_com); + } else if (!TextUtils.isEmpty(mLoginSiteUrl)) { + label.setText(getString(R.string.enter_email_for_site, mLoginSiteUrl)); + } else { + label.setText(R.string.enter_email_to_continue_wordpress_com); + } break; case WOO_LOGIN_MODE: - label.setText(getString(R.string.enter_email_for_site, mLoginSiteUrl)); + if (mOptionalSiteCredsLayout) { + label.setText(getString(R.string.enter_email_for_site, mLoginSiteUrl)); + } else { + label.setText(getString(R.string.enter_email_wordpress_com)); + } break; case JETPACK_STATS: label.setText(R.string.login_to_to_connect_jetpack); @@ -133,7 +218,29 @@ protected void setupLabel(@NonNull TextView label) { protected void setupContent(ViewGroup rootView) { // important for accessibility - talkback getActivity().setTitle(R.string.email_address_login_title); - mEmailInput = rootView.findViewById(R.id.login_email_row); + + setupEmailInput((WPLoginInputRow) rootView.findViewById(R.id.login_email_row)); + + if (mShouldUseNewLayout) { + setupContinueButton((Button) rootView.findViewById(R.id.login_continue_button)); + setupTosButtons( + (Button) rootView.findViewById(R.id.continue_tos), + (Button) rootView.findViewById(R.id.continue_with_google_tos)); + setupSocialButtons((Button) rootView.findViewById(R.id.continue_with_google)); + } else if (mOptionalSiteCredsLayout) { + setupContinueButton((Button) rootView.findViewById(R.id.login_continue_button)); + setupSiteCredsButton((Button) rootView.findViewById(R.id.login_site_creds)); + setupFindEmailHelpButton( + (Button) rootView.findViewById(R.id.login_find_connected_email)); + } else { + setupAlternativeButtons( + (LinearLayout) rootView.findViewById(R.id.login_google_button), + (LinearLayout) rootView.findViewById(R.id.login_site_button)); + } + } + + private void setupEmailInput(WPLoginInputRow emailInput) { + mEmailInput = emailInput; if (BuildConfig.DEBUG) { mEmailInput.getEditText().setText(BuildConfig.DEBUG_WPCOM_LOGIN_EMAIL); } @@ -148,42 +255,107 @@ protected void setupContent(ViewGroup rootView) { @Override public void onFocusChange(View view, boolean hasFocus) { if (hasFocus && !mIsDisplayingEmailHints && !mHasDismissedEmailHints) { - mIsDisplayingEmailHints = true; - getEmailHints(); + mAnalyticsListener.trackSelectEmailField(); + showHintPickerDialogIfNeeded(); } } }); mEmailInput.getEditText().setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { + mAnalyticsListener.trackSelectEmailField(); if (!mIsDisplayingEmailHints && !mHasDismissedEmailHints) { - mIsDisplayingEmailHints = true; - getEmailHints(); + mAnalyticsListener.trackSelectEmailField(); + showHintPickerDialogIfNeeded(); } } }); + } - LinearLayout googleLoginButton = rootView.findViewById(R.id.login_google_button); - googleLoginButton.setOnClickListener(new OnClickListener() { - @Override + private void setupContinueButton(Button continueButton) { + continueButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { - mAnalyticsListener.trackSocialButtonClick(); - ActivityUtils.hideKeyboardForced(mEmailInput.getEditText()); - - if (NetworkUtils.checkConnection(getActivity())) { - if (isAdded()) { - mOldSitesIDs = SiteUtils.getCurrentSiteIds(mSiteStore, false); - mIsSocialLogin = true; - mLoginListener.addGoogleLoginFragment(); - } else { - AppLog.e(T.NUX, "Google login could not be started. LoginEmailFragment was not attached."); - showErrorDialog(getString(R.string.login_error_generic_start)); + onContinueClicked(); + } + }); + } + + private void updateContinueButtonEnabledStatus() { + View view = getView(); + if (view != null) { + Button continueButton = (Button) view.findViewById(R.id.login_continue_button); + String currentEmail = mEmailInput.getEditText().getText().toString(); + continueButton.setEnabled(!currentEmail.trim().isEmpty()); + } + } + + @Override public void onDestroyView() { + mEmailInput = null; + + super.onDestroyView(); + } + + private void setupTosButtons(Button continueTosButton, Button continueWithGoogleTosButton) { + if (mHideTos) { + // Hide the TOS buttons + continueTosButton.setVisibility(View.GONE); + continueWithGoogleTosButton.setVisibility(View.GONE); + } else { + // Show the TOS buttons + continueTosButton.setVisibility(View.VISIBLE); + continueWithGoogleTosButton.setVisibility(View.VISIBLE); + + OnClickListener onClickListener = new OnClickListener() { + public void onClick(View view) { + Context context = getContext(); + if ((context instanceof SignupSheetListener)) { + ((SignupSheetListener) context).onSignupSheetTermsOfServiceClicked(); } } + }; + + continueTosButton.setOnClickListener(onClickListener); + continueTosButton.setText(formatTosText(R.string.continue_terms_of_service_text)); + + continueWithGoogleTosButton.setOnClickListener(onClickListener); + continueWithGoogleTosButton + .setText(formatTosText(R.string.continue_with_google_terms_of_service_text)); + } + } + + private void setupSocialButtons(Button continueWithGoogleButton) { + continueWithGoogleButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + onGoogleSigninClicked(); + } + }); + } + + private void setupSiteCredsButton(Button continueWithSiteCreds) { + continueWithSiteCreds.setOnClickListener(new OnClickListener() { + @Override public void onClick(View v) { + mLoginListener.loginViaSiteCredentials(mLoginSiteUrl); + } + }); + } + + private void setupFindEmailHelpButton(Button findConnectedEmail) { + findConnectedEmail.setOnClickListener(new OnClickListener() { + @Override public void onClick(View v) { + mLoginListener.showHelpFindingConnectedEmail(); + } + }); + } + + private void setupAlternativeButtons(LinearLayout googleLoginButton, LinearLayout siteLoginButton) { + googleLoginButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + onGoogleSigninClicked(); } }); - LinearLayout siteLoginButton = rootView.findViewById(R.id.login_site_button); siteLoginButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { @@ -200,8 +372,8 @@ public void onClick(View view) { } }); - ImageView siteLoginButtonIcon = rootView.findViewById(R.id.login_site_button_icon); - TextView siteLoginButtonText = rootView.findViewById(R.id.login_site_button_text); + ImageView siteLoginButtonIcon = siteLoginButton.findViewById(R.id.login_site_button_icon); + TextView siteLoginButtonText = siteLoginButton.findViewById(R.id.login_site_button_text); switch (mLoginListener.getLoginMode()) { case WOO_LOGIN_MODE: @@ -211,6 +383,7 @@ public void onClick(View view) { case FULL: case WPCOM_LOGIN_ONLY: case SHARE_INTENT: + siteLoginButton.setVisibility(mIsSiteLoginEnabled ? View.VISIBLE : View.GONE); siteLoginButtonIcon.setImageResource(R.drawable.ic_domains_grey_24dp); siteLoginButtonText.setText(R.string.enter_site_address_instead); break; @@ -227,9 +400,19 @@ public void onClick(View view) { @Override protected void setupBottomButtons(Button secondaryButton, Button primaryButton) { - if (mLoginListener.getLoginMode() == LoginMode.JETPACK_STATS) { - secondaryButton.setText(Html.fromHtml(String.format(getResources().getString( - R.string.login_email_button_signup), "", ""))); + if (mShouldUseNewLayout || mOptionalSiteCredsLayout) { + secondaryButton.setVisibility(View.GONE); + primaryButton.setVisibility(View.GONE); + } else { + setupSecondaryButton(secondaryButton); + setupPrimaryButton(primaryButton); + } + } + + private void setupSecondaryButton(Button secondaryButton) { + // Show Sign-Up button if login mode is Jetpack and signup from login is not enabled + if (mLoginListener.getLoginMode() == LoginMode.JETPACK_STATS && !mIsSignupFromLoginEnabled) { + secondaryButton.setText(formatUnderlinedText(R.string.login_email_button_signup)); secondaryButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { mLoginListener.doStartSignup(); @@ -250,14 +433,46 @@ public void onClick(View view) { } else { secondaryButton.setVisibility(View.GONE); } + } + private void setupPrimaryButton(Button primaryButton) { primaryButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { - next(getCleanedEmail()); + onContinueClicked(); } }); } + private Spanned formatTosText(int stringResId) { + final int primaryColorResId = ContextExtensionsKt.getColorResIdFromAttribute(getContext(), R.attr.colorPrimary); + final String primaryColorHtml = HtmlUtils.colorResToHtmlColor(getContext(), primaryColorResId); + return Html.fromHtml(getString(stringResId, "", "")); + } + + private Spanned formatUnderlinedText(int stringResId) { + return Html.fromHtml(getString(stringResId, "", "")); + } + + private void onContinueClicked() { + next(getCleanedEmail()); + } + + private void onGoogleSigninClicked() { + mAnalyticsListener.trackSocialButtonClick(); + ActivityUtils.hideKeyboardForced(mEmailInput.getEditText()); + + if (NetworkUtils.checkConnection(getActivity())) { + if (isAdded()) { + mOldSitesIDs = SiteUtils.getCurrentSiteIds(mSiteStore, false); + mIsSocialLogin = true; + mLoginListener.addGoogleLoginFragment(mIsSignupFromLoginEnabled); + } else { + AppLog.e(T.NUX, "Google login could not be started. LoginEmailFragment was not attached."); + showErrorDialog(getString(R.string.login_error_generic_start)); + } + } + } + @Override protected void onHelp() { if (mLoginListener != null) { @@ -284,6 +499,11 @@ public void onCreate(Bundle savedInstanceState) { Bundle args = getArguments(); if (args != null) { mLoginSiteUrl = args.getString(ARG_LOGIN_SITE_URL, ""); + mIsSignupFromLoginEnabled = args.getBoolean(ARG_SIGNUP_FROM_LOGIN_ENABLED, false); + mIsSiteLoginEnabled = args.getBoolean(ARG_SITE_LOGIN_ENABLED, true); + mShouldUseNewLayout = args.getBoolean(ARG_SHOULD_USE_NEW_LAYOUT, false); + mOptionalSiteCredsLayout = args.getBoolean(ARG_OPTIONAL_SITE_CREDS_LAYOUT, false); + mHideTos = args.getBoolean(ARG_HIDE_TOS, false); } } @@ -328,6 +548,15 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { } } + @Override + public void onResume() { + super.onResume(); + mAnalyticsListener.emailFormScreenResumed(); + if (mShouldUseNewLayout || mOptionalSiteCredsLayout) { + updateContinueButtonEnabledStatus(); + } + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -342,7 +571,19 @@ public void onSaveInstanceState(Bundle outState) { } } + @Override + protected void buildToolbar(Toolbar toolbar, ActionBar actionBar) { + if (mShouldUseNewLayout) { + actionBar.setTitle(R.string.get_started); + } else if (mOptionalSiteCredsLayout) { + actionBar.setTitle(R.string.log_in); + } else { + super.buildToolbar(toolbar, actionBar); + } + } + protected void next(String email) { + mAnalyticsListener.trackSubmitClicked(); if (!NetworkUtils.checkConnection(getActivity())) { return; } @@ -351,7 +592,7 @@ protected void next(String email) { clearEmailError(); startProgress(); mRequestedEmail = email; - mDispatcher.dispatch(AccountActionBuilder.newIsAvailableEmailAction(email)); + mDispatcher.dispatch(AccountActionBuilder.newFetchAuthOptionsAction(new FetchAuthOptionsPayload(email))); } else { showEmailError(R.string.email_invalid); } @@ -367,7 +608,7 @@ private void clearEmailError() { private void showEmailError() { if (mCurrentEmailErrorRes != null) { - showEmailError(mCurrentEmailErrorRes); + showEmailError(mCurrentEmailErrorRes); } } @@ -406,11 +647,16 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { mEmailInput.setError(null); mIsSocialLogin = false; clearEmailError(); + if (mShouldUseNewLayout || mOptionalSiteCredsLayout) { + updateContinueButtonEnabledStatus(); + } } private void showEmailError(int messageId) { mCurrentEmailErrorRes = messageId; - mEmailInput.setError(getString(messageId)); + String errorMessage = getString(messageId); + mAnalyticsListener.trackFailure(errorMessage); + mEmailInput.setError(errorMessage); } private void showErrorDialog(String message) { @@ -431,44 +677,65 @@ protected void endProgress() { @SuppressWarnings("unused") @Subscribe(threadMode = ThreadMode.MAIN) - public void onAvailabilityChecked(OnAvailabilityChecked event) { - if (mRequestedEmail == null || !mRequestedEmail.equalsIgnoreCase(event.value)) { - // bail if user canceled or a different email request is outstanding + public void onAuthOptionsFetched(OnAuthOptionsFetched event) { + if (mRequestedEmail == null) { + // bail if user canceled return; } + final String email = mRequestedEmail; + if (isInProgress()) { endProgress(); } + // hide the keyboard + ActivityUtils.hideKeyboardForced(mEmailInput); + if (event.isError()) { // report the error but don't bail yet. - AppLog.e(T.API, "OnAvailabilityChecked has error: " + event.error.type + " - " + event.error.message); - // hide the keyboard to ensure the link to login using the site address is visible - ActivityUtils.hideKeyboardForced(mEmailInput); - // we validate the email prior to making the request, but just to be safe... - if (event.error.type == AccountStore.IsAvailableErrorType.INVALID) { - showEmailError(R.string.email_invalid); - } else { - showErrorDialog(getString(R.string.error_generic_network)); - } - return; - } + AppLog.e(T.API, "OnAuthOptionsFetched has error: " + event.error.type + " - " + event.error.message); + + switch (event.error.type) { + case UNKNOWN_USER: + // This email does not correspond to an existing account + + // Will be true if in the Woo app and currently in the WPcom login + // flow. We need to check this to know if we should display the + // 'No WPcom account found' error screen. + boolean isWooWPcomLoginFlow = false; + if (mLoginListener != null + && mLoginListener.getLoginMode() == LoginMode.WOO_LOGIN_MODE + && !mOptionalSiteCredsLayout) { + isWooWPcomLoginFlow = true; + } - switch (event.type) { - case EMAIL: - if (event.isAvailable) { - // email address is available on wpcom, so apparently the user can't login with that one. - ActivityUtils.hideKeyboardForced(mEmailInput); - showEmailError(R.string.email_not_registered_wpcom); - } else if (mLoginListener != null) { - ActivityUtils.hideKeyboardForced(mEmailInput); - mLoginListener.gotWpcomEmail(event.value, false); - } - break; - default: - AppLog.e(T.API, "OnAvailabilityChecked unhandled event type: " + event.error.type); - break; + if (mIsSignupFromLoginEnabled || isWooWPcomLoginFlow) { + if (mLoginListener != null) { + mLoginListener.gotUnregisteredEmail(email); + } + } else { + mAnalyticsListener.trackFailure("Email not registered WP.com"); + showEmailError(R.string.email_not_registered_wpcom); + } + break; + case EMAIL_LOGIN_NOT_ALLOWED: + // As a security measure, this user needs to log in using an username and password + mAnalyticsListener.trackFailure("Login with username required"); + ToastUtils.showToast(getContext(), R.string.error_user_username_instead_of_email, Duration.LONG); + if (mLoginListener != null) { + mLoginListener.loginViaWpcomUsernameInstead(); + } + break; + case GENERIC_ERROR: + default: + showErrorDialog(getString(R.string.error_generic_network)); + } + } else { + if (mLoginListener != null) { + mLoginListener + .gotWpcomEmail(email, false, new AuthOptions(event.isPasswordless, event.isEmailVerified)); + } } } @@ -501,7 +768,22 @@ public void onConnectionSuspended(int i) { AppLog.d(T.NUX, LOG_TAG + ": Google API client connection suspended"); } - public void getEmailHints() { + private void showHintPickerDialogIfNeeded() { + // If autofill is available and enabled, we favor the active autofill service over the hint picker dialog. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + final AutofillManager autofillManager = requireContext().getSystemService(AutofillManager.class); + if (autofillManager != null && autofillManager.isEnabled()) { + AppLog.d(T.NUX, LOG_TAG + ": Autofill framework is enabled. Disabling hint picker dialog."); + return; + } + } + + AppLog.d(T.NUX, LOG_TAG + ": Autofill framework is unavailable or disabled. Showing hint picker dialog."); + + showHintPickerDialog(); + } + + private void showHintPickerDialog() { GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance(); if (getContext() == null || googleApiAvailability.isGooglePlayServicesAvailable(getContext()) != ConnectionResult.SUCCESS) { @@ -519,6 +801,7 @@ public void getEmailHints() { try { startIntentSenderForResult(intent.getIntentSender(), EMAIL_CREDENTIALS_REQUEST_CODE, null, 0, 0, 0, null); + mIsDisplayingEmailHints = true; } catch (IntentSender.SendIntentException exception) { AppLog.d(T.NUX, LOG_TAG + "Could not start email hint picker" + exception); } catch (ActivityNotFoundException exception) { diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginEmailPasswordFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginEmailPasswordFragment.java index e32c9a12ebe3..77a77d190199 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginEmailPasswordFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginEmailPasswordFragment.java @@ -10,16 +10,22 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; +import android.widget.ImageView; +import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.Toolbar; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.wordpress.android.login.LoginWpcomService.LoginState; import org.wordpress.android.login.LoginWpcomService.OnCredentialsOK; +import org.wordpress.android.login.util.AvatarHelper; +import org.wordpress.android.login.util.AvatarHelper.AvatarRequestListener; import org.wordpress.android.login.util.SiteUtils; import org.wordpress.android.login.widgets.WPLoginInputRow; import org.wordpress.android.login.widgets.WPLoginInputRow.OnEditorCommitListener; @@ -44,6 +50,8 @@ public class LoginEmailPasswordFragment extends LoginBaseFormFragment oldSiteIds, boolean doLoginUpdate); @@ -26,13 +30,12 @@ interface SelfSignedSSLCallback { void loginViaSiteCredentials(String inputSiteAddress); void helpEmailScreen(String email); void helpSocialEmailScreen(String email); - void addGoogleLoginFragment(); + void addGoogleLoginFragment(boolean isSignupFromLoginEnabled); void showHelpFindingConnectedEmail(); // Login Request Magic Link callbacks - void showMagicLinkSentScreen(String email); + void showMagicLinkSentScreen(String email, boolean allowPassword); void usePasswordInstead(String email); - void forgotPassword(String url); void helpMagicLinkRequest(String email); // Login Magic Link Sent callbacks @@ -40,6 +43,8 @@ interface SelfSignedSSLCallback { void helpMagicLinkSent(String email); // Login email password callbacks + void forgotPassword(String url); + void useMagicLinkInstead(String email, boolean verifyEmail); void needs2fa(String email, String password); void needs2faSocial(String email, String userId, String nonceAuthenticator, String nonceBackup, String nonceSms); void needs2faSocialConnect(String email, String password, String idToken, String service); @@ -48,7 +53,7 @@ interface SelfSignedSSLCallback { // Login Site Address input callbacks void alreadyLoggedInWpcom(ArrayList oldSitesIds); - void gotWpcomSiteInfo(String siteAddress, String siteName, String siteIconUrl); + void gotWpcomSiteInfo(String siteAddress); void gotConnectedSiteInfo(@NonNull String siteAddress, @Nullable String redirectUrl, boolean hasJetpack); void gotXmlRpcEndpoint(String inputSiteAddress, String endpointAddress); void handleSslCertificateError(MemorizingTrustManager memorizingTrustManager, SelfSignedSSLCallback callback); @@ -76,6 +81,8 @@ void helpHandleDiscoveryError(String siteAddress, String endpointAddress, String void doStartSignup(); void helpSignupEmailScreen(String email); void helpSignupMagicLinkScreen(String email); + void helpSignupConfirmationScreen(String email); void showSignupMagicLink(String email); + void showSignupSocial(String email, String displayName, String idToken, String photoUrl, String service); void showSignupToLoginMessage(); } diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginMagicLinkRequestFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginMagicLinkRequestFragment.java index bb199bf19c0d..ccf6f94482cc 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginMagicLinkRequestFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginMagicLinkRequestFragment.java @@ -3,7 +3,6 @@ import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; -import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.Html; import android.view.LayoutInflater; @@ -12,7 +11,6 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; @@ -22,13 +20,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.DataSource; -import com.bumptech.glide.load.engine.GlideException; -import com.bumptech.glide.request.RequestListener; -import com.bumptech.glide.request.RequestOptions; -import com.bumptech.glide.request.target.Target; +import androidx.fragment.app.FragmentManager; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -39,8 +31,9 @@ import org.wordpress.android.fluxc.store.AccountStore.AuthEmailPayloadScheme; import org.wordpress.android.fluxc.store.AccountStore.AuthEmailPayloadSource; import org.wordpress.android.fluxc.store.AccountStore.OnAuthEmailSent; +import org.wordpress.android.login.util.AvatarHelper; +import org.wordpress.android.login.util.AvatarHelper.AvatarRequestListener; import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.GravatarUtils; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; @@ -59,6 +52,8 @@ public class LoginMagicLinkRequestFragment extends Fragment { private static final String ARG_IS_JETPACK_CONNECT = "ARG_IS_JETPACK_CONNECT"; private static final String ARG_JETPACK_CONNECT_SOURCE = "ARG_JETPACK_CONNECT_SOURCE"; private static final String ARG_VERIFY_MAGIC_LINK_EMAIL = "ARG_VERIFY_MAGIC_LINK_EMAIL"; + private static final String ARG_ALLOW_PASSWORD = "ARG_ALLOW_PASSWORD"; + private static final String ARG_FORCE_REQUEST_AT_START = "ARG_FORCE_REQUEST_AT_START"; private static final String ERROR_KEY = "error"; @@ -75,13 +70,23 @@ public class LoginMagicLinkRequestFragment extends Fragment { private boolean mInProgress; private boolean mIsJetpackConnect; private boolean mVerifyMagicLinkEmail; + private boolean mAllowPassword; + private boolean mForceRequestAtStart; @Inject protected Dispatcher mDispatcher; @Inject protected LoginAnalyticsListener mAnalyticsListener; + public static LoginMagicLinkRequestFragment newInstance(String email, AuthEmailPayloadScheme scheme, boolean isJetpackConnect, String jetpackConnectSource, boolean verifyEmail) { + return newInstance(email, scheme, isJetpackConnect, jetpackConnectSource, verifyEmail, true, false); + } + + public static LoginMagicLinkRequestFragment newInstance(String email, AuthEmailPayloadScheme scheme, + boolean isJetpackConnect, String jetpackConnectSource, + boolean verifyEmail, boolean allowPassword, + boolean forceRequestAtStart) { LoginMagicLinkRequestFragment fragment = new LoginMagicLinkRequestFragment(); Bundle args = new Bundle(); args.putString(ARG_EMAIL_ADDRESS, email); @@ -89,6 +94,8 @@ public static LoginMagicLinkRequestFragment newInstance(String email, AuthEmailP args.putBoolean(ARG_IS_JETPACK_CONNECT, isJetpackConnect); args.putString(ARG_JETPACK_CONNECT_SOURCE, jetpackConnectSource); args.putBoolean(ARG_VERIFY_MAGIC_LINK_EMAIL, verifyEmail); + args.putBoolean(ARG_ALLOW_PASSWORD, allowPassword); + args.putBoolean(ARG_FORCE_REQUEST_AT_START, forceRequestAtStart); fragment.setArguments(args); return fragment; } @@ -114,6 +121,8 @@ public void onCreate(Bundle savedInstanceState) { mIsJetpackConnect = getArguments().getBoolean(ARG_IS_JETPACK_CONNECT); mJetpackConnectSource = getArguments().getString(ARG_JETPACK_CONNECT_SOURCE); mVerifyMagicLinkEmail = getArguments().getBoolean(ARG_VERIFY_MAGIC_LINK_EMAIL); + mAllowPassword = getArguments().getBoolean(ARG_ALLOW_PASSWORD); + mForceRequestAtStart = getArguments().getBoolean(ARG_FORCE_REQUEST_AT_START); } setHasOptionsMenu(true); @@ -126,22 +135,17 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mRequestMagicLinkButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (mLoginListener != null) { - if (NetworkUtils.checkConnection(getActivity())) { - showMagicLinkRequestProgressDialog(); - AuthEmailPayloadSource source = getAuthEmailPayloadSource(); - AuthEmailPayload authEmailPayload = new AuthEmailPayload(mEmail, false, - mIsJetpackConnect ? AccountStore.AuthEmailPayloadFlow.JETPACK : null, - source, mMagicLinkScheme); - mDispatcher.dispatch(AuthenticationActionBuilder.newSendAuthEmailAction(authEmailPayload)); - } - } + mAnalyticsListener.trackRequestMagicLinkClick(); + dispatchMagicLinkRequest(); } }); - view.findViewById(R.id.login_enter_password).setOnClickListener(new View.OnClickListener() { + final Button passwordButton = view.findViewById(R.id.login_enter_password); + passwordButton.setVisibility(mAllowPassword ? View.VISIBLE : View.GONE); + passwordButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { + mAnalyticsListener.trackLoginWithPasswordClick(); if (mLoginListener != null) { mLoginListener.usePasswordInstead(mEmail); } @@ -151,46 +155,27 @@ public void onClick(View v) { mAvatarProgressBar = view.findViewById(R.id.avatar_progress); ImageView avatarView = view.findViewById(R.id.gravatar); - // Design changes added to the Woo Magic link sign-in - if (mVerifyMagicLinkEmail) { - View avatarContainerView = view.findViewById(R.id.avatar_container); + TextView emailView = view.findViewById(R.id.email); + emailView.setText(mEmail); - LayoutParams lp = avatarContainerView.getLayoutParams(); - lp.width = LayoutParams.WRAP_CONTENT; - lp.height = getContext().getResources().getDimensionPixelSize(R.dimen.magic_link_sent_illustration_sz); - avatarContainerView.setLayoutParams(lp); + // Design changes added to the Woo Magic link sign-in - mAvatarProgressBar.setVisibility(View.GONE); - avatarView.setImageResource(R.drawable.login_email_alert); + if (mVerifyMagicLinkEmail) { + AvatarHelper.loadAvatarFromEmail(this, mEmail, avatarView, new AvatarRequestListener() { + @Override public void onRequestFinished() { + mAvatarProgressBar.setVisibility(View.GONE); + } + }); TextView labelTextView = view.findViewById(R.id.label); labelTextView.setText(Html.fromHtml(String.format(getResources().getString( R.string.login_site_credentials_magic_link_label), mEmail))); - - mRequestMagicLinkButton.setText(getString(R.string.send_verification_email)); } else { - Glide.with(this) - .load(GravatarUtils.gravatarFromEmail(mEmail, - getContext().getResources().getDimensionPixelSize(R.dimen.avatar_sz_login))) - .apply(RequestOptions.circleCropTransform()) - .apply(RequestOptions.placeholderOf(R.drawable.ic_gridicons_user_circle_100dp)) - .apply(RequestOptions.errorOf(R.drawable.ic_gridicons_user_circle_100dp)) - .listener(new RequestListener() { - @Override - public boolean onLoadFailed(@Nullable GlideException e, Object o, Target target, - boolean b) { - mAvatarProgressBar.setVisibility(View.GONE); - return false; - } - - @Override - public boolean onResourceReady(Drawable drawable, Object o, Target target, - DataSource dataSource, boolean b) { - mAvatarProgressBar.setVisibility(View.GONE); - return false; - } - }) - .into(avatarView); + AvatarHelper.loadAvatarFromEmail(this, mEmail, avatarView, new AvatarRequestListener() { + @Override public void onRequestFinished() { + mAvatarProgressBar.setVisibility(View.GONE); + } + }); } return view; @@ -205,13 +190,17 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); if (actionBar != null) { - actionBar.setDisplayShowTitleEnabled(false); + actionBar.setTitle(R.string.log_in); actionBar.setDisplayHomeAsUpEnabled(true); } if (savedInstanceState == null) { mAnalyticsListener.trackMagicLinkRequestFormViewed(); } + + if (mForceRequestAtStart && !mInProgress) { + dispatchMagicLinkRequest(); + } } @Override @@ -228,12 +217,23 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { getActivity().setTitle(R.string.magic_link_login_title); } + @Override public void onResume() { + super.onResume(); + mAnalyticsListener.magicLinkRequestScreenResumed(); + } + @Override public void onDetach() { super.onDetach(); mLoginListener = null; } + @Override public void onDestroyView() { + mRequestMagicLinkButton = null; + + super.onDestroyView(); + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -249,6 +249,7 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.help) { + mAnalyticsListener.trackShowHelpClick(); if (mLoginListener != null) { mLoginListener.helpMagicLinkRequest(mEmail); } @@ -270,6 +271,19 @@ public void onStop() { mDispatcher.unregister(this); } + private void dispatchMagicLinkRequest() { + if (mLoginListener != null) { + if (NetworkUtils.checkConnection(getActivity())) { + showMagicLinkRequestProgressDialog(); + AuthEmailPayloadSource source = getAuthEmailPayloadSource(); + AuthEmailPayload authEmailPayload = new AuthEmailPayload(mEmail, false, + mIsJetpackConnect ? AccountStore.AuthEmailPayloadFlow.JETPACK : null, + source, mMagicLinkScheme); + mDispatcher.dispatch(AuthenticationActionBuilder.newSendAuthEmailAction(authEmailPayload)); + } + } + } + private AuthEmailPayloadSource getAuthEmailPayloadSource() { if (mJetpackConnectSource != null) { if (mJetpackConnectSource.equalsIgnoreCase(AuthEmailPayloadSource.NOTIFICATIONS.toString())) { @@ -330,6 +344,7 @@ public void onAuthEmailSent(OnAuthEmailSent event) { HashMap errorProperties = new HashMap<>(); errorProperties.put(ERROR_KEY, event.error.message); mAnalyticsListener.trackMagicLinkFailed(errorProperties); + mAnalyticsListener.trackFailure(event.error.message); AppLog.e(AppLog.T.API, "OnAuthEmailSent has error: " + event.error.type + " - " + event.error.message); if (isAdded()) { @@ -342,7 +357,15 @@ public void onAuthEmailSent(OnAuthEmailSent event) { mAnalyticsListener.trackMagicLinkRequested(); if (mLoginListener != null) { - mLoginListener.showMagicLinkSentScreen(mEmail); + // when magic link request if forced we want to remove this fragment from backstack so user will not be + // able to navigate back to it from "Magic Link Sent" Screen + if (mForceRequestAtStart) { + FragmentManager fragmentManager = getFragmentManager(); + if (fragmentManager != null) { + fragmentManager.popBackStackImmediate(); + } + } + mLoginListener.showMagicLinkSentScreen(mEmail, mAllowPassword); } } } diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginMagicLinkSentFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginMagicLinkSentFragment.java index 54ce0dc1aa18..54fb001e851b 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginMagicLinkSentFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginMagicLinkSentFragment.java @@ -8,6 +8,9 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; @@ -15,6 +18,9 @@ import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; +import org.wordpress.android.login.util.AvatarHelper; +import org.wordpress.android.login.util.AvatarHelper.AvatarRequestListener; + import javax.inject.Inject; import dagger.android.support.AndroidSupportInjection; @@ -23,17 +29,24 @@ public class LoginMagicLinkSentFragment extends Fragment { public static final String TAG = "login_magic_link_sent_fragment_tag"; private static final String ARG_EMAIL_ADDRESS = "ARG_EMAIL_ADDRESS"; + private static final String ARG_ALLOW_PASSWORD = "ARG_ALLOW_PASSWORD"; private LoginListener mLoginListener; private String mEmail; + private boolean mAllowPassword; @Inject protected LoginAnalyticsListener mAnalyticsListener; public static LoginMagicLinkSentFragment newInstance(String email) { + return newInstance(email, true); + } + + public static LoginMagicLinkSentFragment newInstance(String email, boolean allowPassword) { LoginMagicLinkSentFragment fragment = new LoginMagicLinkSentFragment(); Bundle args = new Bundle(); args.putString(ARG_EMAIL_ADDRESS, email); + args.putBoolean(ARG_ALLOW_PASSWORD, allowPassword); fragment.setArguments(args); return fragment; } @@ -43,6 +56,7 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { mEmail = getArguments().getString(ARG_EMAIL_ADDRESS); + mAllowPassword = getArguments().getBoolean(ARG_ALLOW_PASSWORD); } setHasOptionsMenu(true); @@ -61,15 +75,30 @@ public void onClick(View v) { } }); - view.findViewById(R.id.login_enter_password).setOnClickListener(new View.OnClickListener() { + final Button passwordButton = view.findViewById(R.id.login_enter_password); + passwordButton.setVisibility(mAllowPassword ? View.VISIBLE : View.GONE); + passwordButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { + mAnalyticsListener.trackLoginWithPasswordClick(); if (mLoginListener != null) { mLoginListener.usePasswordInstead(mEmail); } } }); + final View avatarProgressBar = view.findViewById(R.id.avatar_progress); + ImageView avatarView = view.findViewById(R.id.gravatar); + + TextView emailView = view.findViewById(R.id.email); + emailView.setText(mEmail); + + AvatarHelper.loadAvatarFromEmail(this, mEmail, avatarView, new AvatarRequestListener() { + @Override public void onRequestFinished() { + avatarProgressBar.setVisibility(View.GONE); + } + }); + return view; } @@ -82,12 +111,12 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); if (actionBar != null) { - actionBar.setDisplayShowTitleEnabled(false); + actionBar.setTitle(R.string.log_in); actionBar.setDisplayHomeAsUpEnabled(true); } if (savedInstanceState == null) { - mAnalyticsListener.trackMagicLinkOpenEmailClientViewed(); + mAnalyticsListener.trackLoginMagicLinkOpenEmailClientViewed(); } } @@ -108,6 +137,11 @@ public void onAttach(Context context) { } } + @Override public void onResume() { + super.onResume(); + mAnalyticsListener.magicLinkSentScreenResumed(); + } + @Override public void onDetach() { super.onDetach(); @@ -122,6 +156,7 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.help) { + mAnalyticsListener.trackShowHelpClick(); if (mLoginListener != null) { mLoginListener.helpMagicLinkSent(mEmail); } diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.java index e6b653cb69d2..3201c09672b0 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.java @@ -16,6 +16,8 @@ import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.Toolbar; import androidx.lifecycle.Observer; import org.greenrobot.eventbus.Subscribe; @@ -26,8 +28,8 @@ import org.wordpress.android.fluxc.network.MemorizingTrustManager; import org.wordpress.android.fluxc.network.discovery.SelfHostedEndpointFinder.DiscoveryError; import org.wordpress.android.fluxc.store.AccountStore; +import org.wordpress.android.fluxc.store.SiteStore.ConnectSiteInfoPayload; import org.wordpress.android.fluxc.store.SiteStore.OnConnectSiteInfoChecked; -import org.wordpress.android.fluxc.store.SiteStore.OnWPComSiteFetched; import org.wordpress.android.login.util.SiteUtils; import org.wordpress.android.login.widgets.WPLoginInputRow; import org.wordpress.android.login.widgets.WPLoginInputRow.OnEditorCommitListener; @@ -39,6 +41,7 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.Map; import javax.inject.Inject; @@ -48,6 +51,16 @@ public class LoginSiteAddressFragment extends LoginBaseDiscoveryFragment impleme OnEditorCommitListener, LoginBaseDiscoveryFragment.LoginBaseDiscoveryListener { private static final String KEY_REQUESTED_SITE_ADDRESS = "KEY_REQUESTED_SITE_ADDRESS"; + private static final String KEY_SITE_INFO_URL = "url"; + private static final String KEY_SITE_INFO_URL_AFTER_REDIRECTS = "url_after_redirects"; + private static final String KEY_SITE_INFO_EXISTS = "exists"; + private static final String KEY_SITE_INFO_HAS_JETPACK = "has_jetpack"; + private static final String KEY_SITE_INFO_IS_JETPACK_ACTIVE = "is_jetpack_active"; + private static final String KEY_SITE_INFO_IS_JETPACK_CONNECTED = "is_jetpack_connected"; + private static final String KEY_SITE_INFO_IS_WORDPRESS = "is_wordpress"; + private static final String KEY_SITE_INFO_IS_WPCOM = "is_wp_com"; + private static final String KEY_SITE_INFO_CALCULATED_HAS_JETPACK = "login_calculated_has_jetpack"; + public static final String TAG = "login_site_address_fragment_tag"; private WPLoginInputRow mSiteAddressInput; @@ -93,17 +106,19 @@ protected void setupContent(ViewGroup rootView) { } mSiteAddressInput.addTextChangedListener(this); mSiteAddressInput.setOnEditorCommitListener(this); - } - @Override - protected void setupBottomButtons(Button secondaryButton, Button primaryButton) { - secondaryButton.setText(R.string.login_site_address_help); - secondaryButton.setOnClickListener(new OnClickListener() { + rootView.findViewById(R.id.login_site_address_help_button).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { + mAnalyticsListener.trackShowHelpClick(); showSiteAddressHelp(); } }); + } + + @Override + protected void setupBottomButtons(Button secondaryButton, Button primaryButton) { + secondaryButton.setVisibility(View.GONE); primaryButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { discover(); @@ -111,6 +126,11 @@ public void onClick(View v) { }); } + @Override + protected void buildToolbar(Toolbar toolbar, ActionBar actionBar) { + actionBar.setTitle(R.string.log_in); + } + @Override protected EditText getEditTextToFocusOnStart() { return mSiteAddressInput.getEditText(); @@ -157,6 +177,12 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { }); } + @Override public void onResume() { + super.onResume(); + + mAnalyticsListener.siteAddressFormScreenResumed(); + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -165,14 +191,17 @@ public void onSaveInstanceState(Bundle outState) { } @Override public void onDestroyView() { - super.onDestroyView(); mLoginSiteAddressValidator.dispose(); + mSiteAddressInput = null; + + super.onDestroyView(); } protected void discover() { if (!NetworkUtils.checkConnection(getActivity())) { return; } + mAnalyticsListener.trackSubmitClicked(); mLoginBaseDiscoveryListener = this; @@ -180,12 +209,8 @@ protected void discover() { String cleanedXmlrpcSuffix = UrlUtils.removeXmlrpcSuffix(mRequestedSiteAddress); - if (mLoginListener.getLoginMode() == LoginMode.WOO_LOGIN_MODE) { - mAnalyticsListener.trackConnectedSiteInfoRequested(cleanedXmlrpcSuffix); - mDispatcher.dispatch(SiteActionBuilder.newFetchConnectSiteInfoAction(cleanedXmlrpcSuffix)); - } else { - mDispatcher.dispatch(SiteActionBuilder.newFetchWpcomSiteByUrlAction(cleanedXmlrpcSuffix)); - } + mAnalyticsListener.trackConnectedSiteInfoRequested(cleanedXmlrpcSuffix); + mDispatcher.dispatch(SiteActionBuilder.newFetchConnectSiteInfoAction(cleanedXmlrpcSuffix)); startProgress(); } @@ -212,7 +237,9 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { } private void showError(int messageId) { - mSiteAddressInput.setError(getString(messageId)); + String message = getString(messageId); + mAnalyticsListener.trackFailure(message); + mSiteAddressInput.setError(message); } @Override @@ -255,6 +282,9 @@ public void certificateTrusted() { case MISSING_XMLRPC_METHOD: showError(R.string.xmlrpc_missing_method_error); break; + case WORDPRESS_COM_SITE: + // This is handled by handleWpComDiscoveryError + break; case XMLRPC_BLOCKED: showError(R.string.xmlrpc_post_blocked_error); break; @@ -279,7 +309,7 @@ public void handleWpComDiscoveryError(String failedEndpoint) { ArrayList oldSitesIDs = SiteUtils.getCurrentSiteIds(mSiteStore, true); mLoginListener.alreadyLoggedInWpcom(oldSitesIDs); } else { - mLoginListener.gotWpcomSiteInfo(failedEndpoint, null, null); + mLoginListener.gotWpcomSiteInfo(failedEndpoint); } } @@ -318,46 +348,6 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { // OnChanged events - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onWPComSiteFetched(OnWPComSiteFetched event) { - if (mRequestedSiteAddress == null) { - // bail if user canceled - return; - } - - if (!isAdded()) { - return; - } - - if (event.isError()) { - // Not a WordPress.com or Jetpack site - if (mLoginListener.getLoginMode() == LoginMode.WPCOM_LOGIN_ONLY) { - showError(R.string.enter_wpcom_or_jetpack_site); - endProgress(); - } else { - // Start the discovery process - initiateDiscovery(); - } - } else { - if (event.site.isJetpackInstalled() && mLoginListener.getLoginMode() != LoginMode.WPCOM_LOGIN_ONLY) { - // If Jetpack site, treat it as self-hosted and start the discovery process - // An exception is WPCOM_LOGIN_ONLY mode - in that case we're only interested in adding sites - // through WordPress.com login, and should proceed along that login path - initiateDiscovery(); - return; - } - - endProgress(); - - // it's a wp.com site so, treat it as such. - mLoginListener.gotWpcomSiteInfo( - UrlUtils.removeScheme(event.site.getUrl()), - event.site.getName(), - event.site.getIconUrl()); - } - } - @SuppressWarnings("unused") @Subscribe(threadMode = ThreadMode.MAIN) public void onFetchedConnectSiteInfo(OnConnectSiteInfoChecked event) { @@ -370,16 +360,9 @@ public void onFetchedConnectSiteInfo(OnConnectSiteInfoChecked event) { return; } - // hold the URL in a variable to use below otherwise it gets cleared up by endProgress - final String requestedSiteAddress = mRequestedSiteAddress; - - if (isInProgress()) { - endProgress(); - } - if (event.isError()) { mAnalyticsListener.trackConnectedSiteInfoFailed( - requestedSiteAddress, + mRequestedSiteAddress, event.getClass().getSimpleName(), event.error.type.name(), event.error.message); @@ -387,42 +370,91 @@ public void onFetchedConnectSiteInfo(OnConnectSiteInfoChecked event) { AppLog.e(T.API, "onFetchedConnectSiteInfo has error: " + event.error.message); showError(R.string.invalid_site_url_message); + + endProgressIfNeeded(); } else { - // TODO: If we plan to keep this logic we should convert these labels to constants - HashMap properties = new HashMap<>(); - properties.put("url", event.info.url); - properties.put("url_after_redirects", event.info.urlAfterRedirects); - properties.put("exists", Boolean.toString(event.info.exists)); - properties.put("has_jetpack", Boolean.toString(event.info.hasJetpack)); - properties.put("is_jetpack_active", Boolean.toString(event.info.isJetpackActive)); - properties.put("is_jetpack_connected", Boolean.toString(event.info.isJetpackConnected)); - properties.put("is_wordpress", Boolean.toString(event.info.isWordPress)); - properties.put("is_wp_com", Boolean.toString(event.info.isWPCom)); - - // Determining if jetpack is actually installed takes additional logic. This final - // calculated event property will make querying this event more straight-forward: - boolean hasJetpack = false; - if (event.info.isWPCom && event.info.hasJetpack) { - // This is likely an atomic site. - hasJetpack = true; - } else if (event.info.isJetpackConnected) { - hasJetpack = true; + boolean hasJetpack = calculateHasJetpack(event.info); + + mAnalyticsListener.trackConnectedSiteInfoSucceeded(createConnectSiteInfoProperties(event.info, hasJetpack)); + + if (mLoginListener.getLoginMode() == LoginMode.WOO_LOGIN_MODE) { + handleConnectSiteInfoForWoo(event.info, hasJetpack); + } else { + handleConnectSiteInfoForWordPress(event.info, hasJetpack); } - properties.put("login_calculated_has_jetpack", Boolean.toString(hasJetpack)); - mAnalyticsListener.trackConnectedSiteInfoSucceeded(properties); + } + } - if (!event.info.exists) { - // Site does not exist - showError(R.string.invalid_site_url_message); - } else if (!event.info.isWordPress) { - // Not a WordPress site - showError(R.string.enter_wordpress_site); + private void handleConnectSiteInfoForWoo(ConnectSiteInfoPayload siteInfo, boolean hasJetpack) { + endProgressIfNeeded(); + + if (!siteInfo.exists) { + // Site does not exist + showError(R.string.invalid_site_url_message); + } else if (!siteInfo.isWordPress) { + // Not a WordPress site + showError(R.string.enter_wordpress_site); + } else { + mLoginListener.gotConnectedSiteInfo( + siteInfo.url, + siteInfo.urlAfterRedirects, + hasJetpack); + } + } + + private void handleConnectSiteInfoForWordPress(ConnectSiteInfoPayload siteInfo, boolean hasJetpack) { + if (siteInfo.isWPCom || hasJetpack) { + // It's a WordPress.com or a connected Jetpack site + if (mLoginListener.getLoginMode() == LoginMode.SELFHOSTED_ONLY) { + // We're only interested in self-hosted sites + if (hasJetpack) { + // If Jetpack site, treat it as self-hosted and start the discovery process + // Note: This also includes Atomic sites + initiateDiscovery(); + return; + } + } + // It's a WordPress.com or a connected Jetpack site, so treat it as such + endProgressIfNeeded(); + mLoginListener.gotWpcomSiteInfo(UrlUtils.removeScheme(siteInfo.url)); + } else { + // Not a WordPress.com or a connected Jetpack site + if (mLoginListener.getLoginMode() == LoginMode.WPCOM_LOGIN_ONLY) { + // We're only interested in WordPress.com accounts + showError(R.string.enter_wpcom_or_jetpack_site); + endProgressIfNeeded(); } else { - mLoginListener.gotConnectedSiteInfo( - event.info.url, - event.info.urlAfterRedirects, - hasJetpack); + // Start the discovery process + initiateDiscovery(); } } } + + private boolean calculateHasJetpack(ConnectSiteInfoPayload siteInfo) { + // Determining if jetpack is actually installed takes additional logic. This final + // calculated event property will make querying this event more straight-forward. + // Internal reference: p99K0U-1vO-p2#comment-3574 + boolean hasJetpack = false; + if (siteInfo.isWPCom && siteInfo.hasJetpack) { + // This is likely an atomic site. + hasJetpack = true; + } else if (siteInfo.isJetpackConnected) { + hasJetpack = true; + } + return hasJetpack; + } + + private Map createConnectSiteInfoProperties(ConnectSiteInfoPayload siteInfo, boolean hasJetpack) { + HashMap properties = new HashMap<>(); + properties.put(KEY_SITE_INFO_URL, siteInfo.url); + properties.put(KEY_SITE_INFO_URL_AFTER_REDIRECTS, siteInfo.urlAfterRedirects); + properties.put(KEY_SITE_INFO_EXISTS, Boolean.toString(siteInfo.exists)); + properties.put(KEY_SITE_INFO_HAS_JETPACK, Boolean.toString(siteInfo.hasJetpack)); + properties.put(KEY_SITE_INFO_IS_JETPACK_ACTIVE, Boolean.toString(siteInfo.isJetpackActive)); + properties.put(KEY_SITE_INFO_IS_JETPACK_CONNECTED, Boolean.toString(siteInfo.isJetpackConnected)); + properties.put(KEY_SITE_INFO_IS_WORDPRESS, Boolean.toString(siteInfo.isWordPress)); + properties.put(KEY_SITE_INFO_IS_WPCOM, Boolean.toString(siteInfo.isWPCom)); + properties.put(KEY_SITE_INFO_CALCULATED_HAS_JETPACK, Boolean.toString(hasJetpack)); + return properties; + } } diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginSiteAddressHelpDialogFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginSiteAddressHelpDialogFragment.java index b85d4dbd3a4e..f36350bea392 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginSiteAddressHelpDialogFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginSiteAddressHelpDialogFragment.java @@ -58,6 +58,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { alert.setView(getActivity().getLayoutInflater().inflate(R.layout.login_alert_site_address_help, null)); alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { + mAnalyticsListener.trackDismissDialog(); dialog.dismiss(); } }); diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginUsernamePasswordFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginUsernamePasswordFragment.java index 4a334c9c83e6..2e324bb5cace 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginUsernamePasswordFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/LoginUsernamePasswordFragment.java @@ -11,16 +11,14 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; -import android.widget.ImageView; -import android.widget.ScrollView; import android.widget.TextView; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.Toolbar; +import androidx.core.widget.NestedScrollView; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -43,6 +41,7 @@ import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.EditTextUtils; import org.wordpress.android.util.NetworkUtils; +import org.wordpress.android.util.StringUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.UrlUtils; @@ -62,8 +61,6 @@ public class LoginUsernamePasswordFragment extends LoginBaseDiscoveryFragment im private static final String ARG_INPUT_SITE_ADDRESS = "ARG_INPUT_SITE_ADDRESS"; private static final String ARG_ENDPOINT_ADDRESS = "ARG_ENDPOINT_ADDRESS"; - private static final String ARG_SITE_NAME = "ARG_SITE_NAME"; - private static final String ARG_SITE_ICON_URL = "ARG_SITE_ICON_URL"; private static final String ARG_INPUT_USERNAME = "ARG_INPUT_USERNAME"; private static final String ARG_INPUT_PASSWORD = "ARG_INPUT_PASSWORD"; private static final String ARG_IS_WPCOM = "ARG_IS_WPCOM"; @@ -72,7 +69,7 @@ public class LoginUsernamePasswordFragment extends LoginBaseDiscoveryFragment im public static final String TAG = "login_username_password_fragment_tag"; - private ScrollView mScrollView; + private NestedScrollView mScrollView; private WPLoginInputRow mUsernameInput; private WPLoginInputRow mPasswordInput; @@ -88,22 +85,17 @@ public class LoginUsernamePasswordFragment extends LoginBaseDiscoveryFragment im private String mInputSiteAddress; private String mInputSiteAddressWithoutSuffix; private String mEndpointAddress; - private String mSiteName; - private String mSiteIconUrl; private String mInputUsername; private String mInputPassword; private boolean mIsWpcom; public static LoginUsernamePasswordFragment newInstance(String inputSiteAddress, String endpointAddress, - String siteName, String siteIconUrl, String inputUsername, String inputPassword, boolean isWpcom) { LoginUsernamePasswordFragment fragment = new LoginUsernamePasswordFragment(); Bundle args = new Bundle(); args.putString(ARG_INPUT_SITE_ADDRESS, inputSiteAddress); args.putString(ARG_ENDPOINT_ADDRESS, endpointAddress); - args.putString(ARG_SITE_NAME, siteName); - args.putString(ARG_SITE_ICON_URL, siteIconUrl); args.putString(ARG_INPUT_USERNAME, inputUsername); args.putString(ARG_INPUT_PASSWORD, inputPassword); args.putBoolean(ARG_IS_WPCOM, isWpcom); @@ -123,9 +115,11 @@ public static LoginUsernamePasswordFragment newInstance(String inputSiteAddress, @Override protected void setupLabel(@NonNull TextView label) { - if (mLoginListener.getLoginMode() == LoginMode.WOO_LOGIN_MODE) { - label.setText(getString(R.string.enter_credentials_for_site, mInputSiteAddress)); - } + final boolean isWoo = mLoginListener.getLoginMode() == LoginMode.WOO_LOGIN_MODE; + final int labelResId = isWoo ? R.string.enter_credentials_for_site : R.string.enter_account_info_for_site; + final String formattedSiteAddress = + UrlUtils.removeScheme(UrlUtils.removeXmlrpcSuffix(StringUtils.notNullStr(mInputSiteAddress))); + label.setText(getString(labelResId, formattedSiteAddress)); } @Override @@ -134,28 +128,6 @@ protected void setupContent(ViewGroup rootView) { getActivity().setTitle(R.string.selfhosted_site_login_title); mScrollView = rootView.findViewById(R.id.scroll_view); - rootView.findViewById(R.id.login_site_title_static).setVisibility(mIsWpcom ? View.GONE : View.VISIBLE); - rootView.findViewById(R.id.login_blavatar_static).setVisibility(mIsWpcom ? View.GONE : View.VISIBLE); - rootView.findViewById(R.id.login_blavatar).setVisibility(mIsWpcom ? View.VISIBLE : View.GONE); - rootView.findViewById(R.id.label).setVisibility( - (mLoginListener.getLoginMode() == LoginMode.WOO_LOGIN_MODE) ? View.VISIBLE : View.GONE); - - if (mSiteIconUrl != null) { - Glide.with(this) - .load(mSiteIconUrl) - .apply(RequestOptions.placeholderOf(R.drawable.ic_placeholder_blavatar_grey_lighten_20_40dp)) - .apply(RequestOptions.errorOf(R.drawable.ic_placeholder_blavatar_grey_lighten_20_40dp)) - .into(((ImageView) rootView.findViewById(R.id.login_blavatar))); - } - - TextView siteNameView = (rootView.findViewById(R.id.login_site_title)); - siteNameView.setText(mSiteName); - siteNameView.setVisibility(mSiteName != null ? View.VISIBLE : View.GONE); - - TextView siteAddressView = (rootView.findViewById(R.id.login_site_address)); - siteAddressView.setText(UrlUtils.removeScheme(UrlUtils.removeXmlrpcSuffix(mInputSiteAddress))); - siteAddressView.setVisibility(mInputSiteAddress != null ? View.VISIBLE : View.GONE); - mInputSiteAddressWithoutSuffix = (mEndpointAddress == null || mEndpointAddress.isEmpty()) ? mInputSiteAddress : UrlUtils.removeXmlrpcSuffix(mEndpointAddress); @@ -181,12 +153,8 @@ public void onEditorCommit() { mPasswordInput.addTextChangedListener(this); mPasswordInput.setOnEditorCommitListener(this); - } - @Override - protected void setupBottomButtons(Button secondaryButton, Button primaryButton) { - secondaryButton.setText(R.string.forgot_password); - secondaryButton.setOnClickListener(new OnClickListener() { + rootView.findViewById(R.id.login_reset_password).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mLoginListener != null) { @@ -201,6 +169,11 @@ public void onClick(View v) { } } }); + } + + @Override + protected void setupBottomButtons(Button secondaryButton, Button primaryButton) { + secondaryButton.setVisibility(View.GONE); primaryButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { next(); @@ -208,6 +181,11 @@ public void onClick(View v) { }); } + @Override + protected void buildToolbar(Toolbar toolbar, ActionBar actionBar) { + actionBar.setTitle(R.string.log_in); + } + @Override protected EditText getEditTextToFocusOnStart() { return mUsernameInput.getEditText(); @@ -220,6 +198,20 @@ protected void onHelp() { } } + @Override public void onDestroyView() { + if (mPasswordInput != null) { + mPasswordInput.setOnEditorCommitListener(null); + mPasswordInput = null; + } + if (mUsernameInput != null) { + mUsernameInput.setOnEditorCommitListener(null); + mUsernameInput = null; + } + mScrollView = null; + + super.onDestroyView(); + } + @Override public void onAttach(Context context) { AndroidSupportInjection.inject(this); @@ -232,8 +224,6 @@ public void onCreate(Bundle savedInstanceState) { mInputSiteAddress = getArguments().getString(ARG_INPUT_SITE_ADDRESS); mEndpointAddress = getArguments().getString(ARG_ENDPOINT_ADDRESS, null); - mSiteName = getArguments().getString(ARG_SITE_NAME); - mSiteIconUrl = getArguments().getString(ARG_SITE_ICON_URL); mInputUsername = getArguments().getString(ARG_INPUT_USERNAME); mInputPassword = getArguments().getString(ARG_INPUT_PASSWORD); mIsWpcom = getArguments().getBoolean(ARG_IS_WPCOM); @@ -278,7 +268,20 @@ public void onSaveInstanceState(Bundle outState) { outState.putBoolean(KEY_GET_SITE_OPTIONS_INITIATED, mGetSiteOptionsInitiated); } + @Override public void onResume() { + super.onResume(); + mAnalyticsListener.usernamePasswordScreenResumed(); + updatePrimaryButtonEnabledStatus(); + } + + private void updatePrimaryButtonEnabledStatus() { + String currentUsername = mUsernameInput.getEditText().getText().toString(); + String currentPassword = mPasswordInput.getEditText().getText().toString(); + getPrimaryButton().setEnabled(!currentPassword.trim().isEmpty() && !currentUsername.trim().isEmpty()); + } + protected void next() { + mAnalyticsListener.trackSubmitClicked(); if (!NetworkUtils.checkConnection(getActivity())) { return; } @@ -351,6 +354,7 @@ public void beforeTextChanged(CharSequence s, int start, int count, int after) { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { showError(null); + updatePrimaryButtonEnabledStatus(); } @Override @@ -372,6 +376,7 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { @Override public void handleDiscoveryError(DiscoveryError error, String failedEndpoint) { ActivityUtils.hideKeyboard(getActivity()); + mAnalyticsListener.trackFailure(error.name() + " - " + failedEndpoint); if (error == DiscoveryError.HTTP_AUTH_REQUIRED) { mLoginListener.helpNoJetpackScreen(mInputSiteAddress, mEndpointAddress, getCleanedUsername(), mPasswordInput.getEditText().getText().toString(), @@ -411,7 +416,7 @@ private int getDiscoveryErrorMessage(DiscoveryError error) { @Override public void handleWpComDiscoveryError(String failedEndpoint) { AppLog.e(T.API, "Inputted a wpcom address in site address screen. Redirecting to Email screen"); - mLoginListener.gotWpcomSiteInfo(UrlUtils.removeScheme(failedEndpoint), null, null); + mLoginListener.gotWpcomSiteInfo(UrlUtils.removeScheme(failedEndpoint)); } @Override @@ -421,6 +426,7 @@ public void handleDiscoverySuccess(@NonNull String endpointAddress) { } private void showUsernameError(String errorMessage) { + mAnalyticsListener.trackFailure(errorMessage); mUsernameInput.setError(errorMessage); mPasswordInput.setError(null); @@ -431,6 +437,7 @@ private void showUsernameError(String errorMessage) { private void showPasswordError(String errorMessage) { mUsernameInput.setError(null); + mAnalyticsListener.trackFailure(errorMessage); mPasswordInput.setError(errorMessage); if (errorMessage != null) { @@ -441,6 +448,7 @@ private void showPasswordError(String errorMessage) { private void showError(String errorMessage) { mUsernameInput.setError(errorMessage != null ? " " : null); mPasswordInput.setError(errorMessage); + mAnalyticsListener.trackFailure(errorMessage); if (errorMessage != null) { requestScrollToView(mPasswordInput); @@ -618,7 +626,7 @@ public void onSiteChanged(OnSiteChanged event) { return; } - if (mLoginListener.getLoginMode() == LoginMode.WOO_LOGIN_MODE) { + if (!mIsWpcom && mLoginListener.getLoginMode() == LoginMode.WOO_LOGIN_MODE) { SiteModel lastAddedXMLRPCSite = SiteUtils.getXMLRPCSiteByUrl(mSiteStore, mInputSiteAddress); if (lastAddedXMLRPCSite != null) { // the wp.getOptions endpoint is already called @@ -635,7 +643,7 @@ public void onSiteChanged(OnSiteChanged event) { lastAddedXMLRPCSite.getPassword(), mAccountStore.getAccount().getAvatarUrl(), false); } else { - mLoginListener.gotWpcomEmail(userEmail, true); + mLoginListener.gotWpcomEmail(userEmail, true, null); } } else { // Initiate the wp.getOptions endpoint to fetch the jetpack user email @@ -679,3 +687,4 @@ public void onProfileFetched(OnProfileFetched event) { finishLogin(); } } + diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupConfirmationFragment.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupConfirmationFragment.kt new file mode 100644 index 000000000000..24e7f69ca727 --- /dev/null +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupConfirmationFragment.kt @@ -0,0 +1,172 @@ +package org.wordpress.android.login + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import dagger.android.support.AndroidSupportInjection +import kotlinx.android.synthetic.main.login_include_email_header.* +import kotlinx.android.synthetic.main.signup_confirmation_screen.* +import kotlinx.android.synthetic.main.toolbar_login.* +import org.wordpress.android.login.util.AvatarHelper.AvatarRequestListener +import org.wordpress.android.login.util.AvatarHelper.loadAvatarFromEmail +import org.wordpress.android.login.util.AvatarHelper.loadAvatarFromUrl +import javax.inject.Inject + +class SignupConfirmationFragment : Fragment() { + private var mLoginListener: LoginListener? = null + + private var mEmail: String? = null + private var mDisplayName: String? = null + private var mIdToken: String? = null + private var mPhotoUrl: String? = null + private var mService: String? = null + private var mIsSocialSignup: Boolean = false + + @Inject lateinit var mAnalyticsListener: LoginAnalyticsListener + + companion object { + const val TAG = "signup_confirmation_fragment_tag" + + private const val ARG_EMAIL = "ARG_EMAIL" + private const val ARG_SOCIAL_DISPLAY_NAME = "ARG_SOCIAL_DISPLAY_NAME" + private const val ARG_SOCIAL_ID_TOKEN = "ARG_SOCIAL_ID_TOKEN" + private const val ARG_SOCIAL_PHOTO_URL = "ARG_SOCIAL_PHOTO_URL" + private const val ARG_SOCIAL_SERVICE = "ARG_SOCIAL_SERVICE" + private const val ARG_IS_SOCIAL_SIGNUP = "ARG_IS_SOCIAL_SIGNUP" + + @JvmStatic fun newInstance(email: String?): SignupConfirmationFragment { + return SignupConfirmationFragment().apply { + arguments = Bundle().apply { + putString(ARG_EMAIL, email) + putBoolean(ARG_IS_SOCIAL_SIGNUP, false) + } + } + } + + @JvmStatic fun newInstance( + email: String?, + displayName: String?, + idToken: String?, + photoUrl: String?, + service: String? + ): SignupConfirmationFragment { + return SignupConfirmationFragment().apply { + arguments = Bundle().apply { + putString(ARG_EMAIL, email) + putString(ARG_SOCIAL_DISPLAY_NAME, displayName) + putString(ARG_SOCIAL_ID_TOKEN, idToken) + putString(ARG_SOCIAL_PHOTO_URL, photoUrl) + putString(ARG_SOCIAL_SERVICE, service) + putBoolean(ARG_IS_SOCIAL_SIGNUP, true) + } + } + } + } + + override fun onAttach(context: Context) { + AndroidSupportInjection.inject(this) + super.onAttach(context) + if (context !is LoginListener) { + throw RuntimeException("$context must implement LoginListener") + } + mLoginListener = context + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + mEmail = it.getString(ARG_EMAIL) + mDisplayName = it.getString(ARG_SOCIAL_DISPLAY_NAME) + mIdToken = it.getString(ARG_SOCIAL_ID_TOKEN) + mPhotoUrl = it.getString(ARG_SOCIAL_PHOTO_URL) + mService = it.getString(ARG_SOCIAL_SERVICE) + mIsSocialSignup = it.getBoolean(ARG_IS_SOCIAL_SIGNUP) + } + setHasOptionsMenu(true) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.signup_confirmation_screen, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + (activity as AppCompatActivity?)?.apply { + setSupportActionBar(toolbar) + supportActionBar?.apply { + setTitle(R.string.sign_up_label) + setDisplayHomeAsUpEnabled(true) + } + } + + email.text = mEmail + + val avatarRequestListener = object : AvatarRequestListener { + override fun onRequestFinished() { + avatar_progress.visibility = View.GONE + } + } + + if (mIsSocialSignup) { + loadAvatarFromUrl(this, mPhotoUrl, gravatar, avatarRequestListener) + label.setText(R.string.signup_confirmation_message) + signup_confirmation_button.setText(R.string.create_account) + signup_confirmation_button.setOnClickListener { + mAnalyticsListener.trackCreateAccountClick() + mLoginListener?.showSignupSocial(mEmail, mDisplayName, mIdToken, mPhotoUrl, mService) + } + } else { + loadAvatarFromEmail(this, mEmail, gravatar, avatarRequestListener) + label.setText(R.string.signup_confirmation_magic_link_message) + signup_confirmation_button.setText(R.string.send_link_by_email) + signup_confirmation_button.setOnClickListener { + mAnalyticsListener.trackRequestMagicLinkClick() + mLoginListener?.showSignupMagicLink(mEmail) + } + } + + if (savedInstanceState == null) { + if (mIsSocialSignup) { + mAnalyticsListener.trackSocialSignupConfirmationViewed() + } else { + mAnalyticsListener.trackEmailSignupConfirmationViewed() + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + // important for accessibility - talkback + activity?.setTitle(R.string.signup_confirmation_title) + } + + override fun onDetach() { + super.onDetach() + mLoginListener = null + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.menu_login, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.help) { + mAnalyticsListener.trackShowHelpClick() + if (mLoginListener != null) { + mLoginListener?.helpSignupConfirmationScreen(mEmail) + } + return true + } + return false + } +} diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupEmailFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupEmailFragment.java index a492701f015b..8f5c3349772e 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupEmailFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupEmailFragment.java @@ -17,7 +17,9 @@ import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.Toolbar; import com.google.android.gms.auth.api.Auth; import com.google.android.gms.auth.api.credentials.Credential; @@ -71,7 +73,7 @@ public class SignupEmailFragment extends LoginBaseFormFragment im @Override protected @LayoutRes int getContentLayout() { - return R.layout.signup_email_fragment; + return R.layout.signup_email_screen; } @Override @@ -100,6 +102,7 @@ protected void setupContent(ViewGroup rootView) { @Override public void onFocusChange(View view, boolean hasFocus) { if (hasFocus && !mIsDisplayingEmailHints && !mHasDismissedEmailHints) { + mAnalyticsListener.trackSelectEmailField(); mIsDisplayingEmailHints = true; getEmailHints(); } @@ -109,6 +112,7 @@ public void onFocusChange(View view, boolean hasFocus) { @Override public void onClick(View view) { if (!mIsDisplayingEmailHints && !mHasDismissedEmailHints) { + mAnalyticsListener.trackSelectEmailField(); mIsDisplayingEmailHints = true; getEmailHints(); } @@ -130,6 +134,11 @@ public void onClick(View view) { }); } + @Override + protected void buildToolbar(Toolbar toolbar, ActionBar actionBar) { + actionBar.setTitle(R.string.sign_up_label); + } + @Override protected void onHelp() { if (mLoginListener != null) { @@ -176,6 +185,12 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { } } + @Override + public void onResume() { + super.onResume(); + mAnalyticsListener.emailFormScreenResumed(); + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -185,6 +200,7 @@ public void onSaveInstanceState(Bundle outState) { } protected void next(String email) { + mAnalyticsListener.trackSubmitClicked(); if (NetworkUtils.checkConnection(getActivity())) { if (isValidEmail(email)) { startProgress(); @@ -202,6 +218,13 @@ public void onDetach() { mLoginListener = null; } + @Override + public void onDestroyView() { + mEmailInput = null; + + super.onDestroyView(); + } + private String getCleanedEmail() { return EditTextUtils.getText(mEmailInput.getEditText()).trim(); } @@ -240,6 +263,7 @@ protected void showErrorDialog(String message) { } private void showErrorEmail(String message) { + mAnalyticsListener.trackFailure(message); mEmailInput.setError(message); } @@ -271,7 +295,7 @@ public void onAvailabilityChecked(OnAvailabilityChecked event) { } else { mAnalyticsListener.trackSignupEmailToLogin(); mLoginListener.showSignupToLoginMessage(); - mLoginListener.gotWpcomEmail(event.value, false); + mLoginListener.gotWpcomEmail(event.value, false, null); // Kill connections with FluxC and this fragment since the flow is changing to login. mDispatcher.unregister(this); getActivity().getSupportFragmentManager().beginTransaction().remove(this).commit(); @@ -319,6 +343,7 @@ public void getEmailHints() { PendingIntent intent = Auth.CredentialsApi.getHintPickerIntent(mGoogleApiClient, hintRequest); try { + mAnalyticsListener.trackShowEmailHints(); startIntentSenderForResult(intent.getIntentSender(), EMAIL_CREDENTIALS_REQUEST_CODE, null, 0, 0, 0, null); } catch (IntentSender.SendIntentException exception) { AppLog.d(T.NUX, LOG_TAG + "Could not start email hint picker" + exception); @@ -335,10 +360,12 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { return; } if (resultCode == RESULT_OK) { + mAnalyticsListener.trackPickEmailFromHint(); Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY); mEmailInput.getEditText().setText(credential.getId()); next(getCleanedEmail()); } else { + mAnalyticsListener.trackDismissDialog(); mHasDismissedEmailHints = true; mEmailInput.getEditText().postDelayed(new Runnable() { @Override diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupGoogleFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupGoogleFragment.java index 94e30dcb1588..b728c7462ac1 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupGoogleFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupGoogleFragment.java @@ -23,31 +23,58 @@ import org.wordpress.android.util.AppLog.T; import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import dagger.android.support.AndroidSupportInjection; import static android.app.Activity.RESULT_CANCELED; import static android.app.Activity.RESULT_OK; +import static org.wordpress.android.login.LoginAnalyticsListener.CreatedAccountSource.GOOGLE; + +import dagger.android.support.AndroidSupportInjection; public class SignupGoogleFragment extends GoogleFragment { private static final String OLD_SITES_IDS = "old_sites_ids"; private static final String SIGN_UP_REQUESTED = "sign_up_requested"; + + private static final String ARG_GOOGLE_EMAIL = "ARG_GOOGLE_EMAIL"; + private static final String ARG_DISPLAY_NAME = "ARG_DISPLAY_NAME"; + private static final String ARG_ID_TOKEN = "ARG_ID_TOKEN"; + private static final String ARG_PHOTO_URL = "ARG_PHOTO_URL"; + private static final String ARG_FORCE_SIGNUP_AT_START = "ARG_FORCE_SIGNUP_AT_START"; + private ArrayList mOldSitesIds; private ProgressDialog mProgressDialog; private boolean mSignupRequested; + private boolean mForceSignupAtStart; private static final int REQUEST_SIGNUP = 1002; public static final String TAG = "signup_google_fragment_tag"; + public static SignupGoogleFragment newInstance(String email, String displayName, String idToken, String photoUrl) { + SignupGoogleFragment fragment = new SignupGoogleFragment(); + Bundle args = new Bundle(); + args.putString(ARG_GOOGLE_EMAIL, email); + args.putString(ARG_DISPLAY_NAME, displayName); + args.putString(ARG_ID_TOKEN, idToken); + args.putString(ARG_PHOTO_URL, photoUrl); + args.putBoolean(ARG_FORCE_SIGNUP_AT_START, true); + fragment.setArguments(args); + return fragment; + } + @Override public void onAttach(Context context) { AndroidSupportInjection.inject(this); mProgressDialog = ProgressDialog.show( getActivity(), null, getString(R.string.signup_with_google_progress), true, false, null); super.onAttach(context); + Bundle args = getArguments(); + if (args != null) { + mDisplayName = args.getString(ARG_DISPLAY_NAME); + mGoogleEmail = args.getString(ARG_GOOGLE_EMAIL); + mIdToken = args.getString(ARG_ID_TOKEN); + mPhotoUrl = args.getString(ARG_PHOTO_URL); + mForceSignupAtStart = args.getBoolean(ARG_FORCE_SIGNUP_AT_START); + } } @Override public void onCreate(Bundle savedInstanceState) { @@ -72,13 +99,17 @@ public void onDetach() { @Override protected void startFlow() { - if (!mSignupRequested) { - AppLog.d(T.MAIN, "GOOGLE SIGNUP: startFlow"); - mSignupRequested = true; - Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient); - startActivityForResult(signInIntent, REQUEST_SIGNUP); + if (mForceSignupAtStart) { + dispatchSocialSignup(mIdToken); } else { - AppLog.d(T.MAIN, "GOOGLE SIGNUP: startFlow called, but is already in progress"); + if (!mSignupRequested) { + AppLog.d(T.MAIN, "GOOGLE SIGNUP: startFlow"); + mSignupRequested = true; + Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient); + startActivityForResult(signInIntent, REQUEST_SIGNUP); + } else { + AppLog.d(T.MAIN, "GOOGLE SIGNUP: startFlow called, but is already in progress"); + } } } @@ -106,10 +137,9 @@ public void onActivityResult(int request, int result, Intent data) { account.getPhotoUrl() != null ? account.getPhotoUrl().toString() : ""); } - PushSocialPayload payload = new PushSocialPayload(mIdToken, SERVICE_TYPE_GOOGLE); AppLog.d(T.MAIN, "GOOGLE SIGNUP: sign up result returned - dispatching SocialSignupAction"); - mDispatcher.dispatch(AccountActionBuilder.newPushSocialSignupAction(payload)); - mOldSitesIds = SiteUtils.getCurrentSiteIds(mSiteStore, false); + + dispatchSocialSignup(mIdToken); } catch (NullPointerException exception) { AppLog.d(T.MAIN, "GOOGLE SIGNUP: sign up result returned - NPE"); AppLog.e(T.NUX, "Cannot get ID token from Google signup account.", exception); @@ -183,13 +213,10 @@ private void dismissProgressDialog() { } } - // Remove scale from photo URL path string. Current URL matches /s96-c, which returns a 96 x 96 - // pixel image. Removing /s96-c from the string returns a 512 x 512 pixel image. Using regular - // expressions may help if the photo URL scale value in the returned path changes. - private String removeScaleFromGooglePhotoUrl(String photoUrl) { - Pattern pattern = Pattern.compile("(/s[0-9]+-c)"); - Matcher matcher = pattern.matcher(photoUrl); - return matcher.find() ? photoUrl.replace(matcher.group(1), "") : photoUrl; + private void dispatchSocialSignup(String idToken) { + PushSocialPayload payload = new PushSocialPayload(idToken, SERVICE_TYPE_GOOGLE); + mDispatcher.dispatch(AccountActionBuilder.newPushSocialSignupAction(payload)); + mOldSitesIds = SiteUtils.getCurrentSiteIds(mSiteStore, false); } @SuppressWarnings("unused") @@ -202,7 +229,7 @@ public void onAuthenticationChanged(OnAuthenticationChanged event) { } else if (event.createdAccount) { AppLog.d(T.MAIN, "GOOGLE SIGNUP: onAuthenticationChanged - new wordpress account created"); - mAnalyticsListener.trackCreatedAccount(event.userName, mGoogleEmail); + mAnalyticsListener.trackCreatedAccount(event.userName, mGoogleEmail, GOOGLE); mAnalyticsListener.trackAnalyticsSignIn(true); mGoogleListener.onGoogleSignupFinished(mDisplayName, mGoogleEmail, mPhotoUrl, event.userName); // Continue with login since existing account was selected. diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupMagicLinkFragment.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupMagicLinkFragment.java index 910cd28db95c..1c4d86dc306f 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupMagicLinkFragment.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/SignupMagicLinkFragment.java @@ -80,7 +80,7 @@ public void onCreate(Bundle savedInstanceState) { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.signup_magic_link, container, false); + View layout = inflater.inflate(R.layout.signup_magic_link_screen, container, false); mOpenMailButton = layout.findViewById(R.id.signup_magic_link_button); mOpenMailButton.setOnClickListener(new View.OnClickListener() { @@ -113,12 +113,12 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); if (actionBar != null) { - actionBar.setDisplayShowTitleEnabled(false); + actionBar.setTitle(R.string.sign_up_label); actionBar.setDisplayHomeAsUpEnabled(true); } if (savedInstanceState == null) { - mAnalyticsListener.trackMagicLinkOpenEmailClientViewed(); + mAnalyticsListener.trackSignupMagicLinkOpenEmailClientViewed(); } } @@ -157,6 +157,7 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.help) { + mAnalyticsListener.trackShowHelpClick(); if (mLoginListener != null) { mLoginListener.helpSignupMagicLinkScreen(mEmail); } @@ -220,6 +221,7 @@ private AuthEmailPayloadSource getAuthEmailPayloadSource() { } protected void showErrorDialog(String message) { + mAnalyticsListener.trackFailure(message); DialogInterface.OnClickListener dialogListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/di/LoginFragmentModule.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/di/LoginFragmentModule.java index 85799f5c65ec..cce15c42a095 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/di/LoginFragmentModule.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/di/LoginFragmentModule.java @@ -9,6 +9,7 @@ import org.wordpress.android.login.LoginSiteAddressFragment; import org.wordpress.android.login.LoginSiteAddressHelpDialogFragment; import org.wordpress.android.login.LoginUsernamePasswordFragment; +import org.wordpress.android.login.SignupConfirmationFragment; import org.wordpress.android.login.SignupEmailFragment; import org.wordpress.android.login.SignupGoogleFragment; import org.wordpress.android.login.SignupMagicLinkFragment; @@ -53,4 +54,7 @@ public abstract class LoginFragmentModule { @ContributesAndroidInjector abstract SignupMagicLinkFragment signupMagicLinkFragment(); + + @ContributesAndroidInjector + abstract SignupConfirmationFragment signupConfirmationScreen(); } diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/util/AvatarHelper.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/util/AvatarHelper.kt new file mode 100644 index 000000000000..f44ad461d0d3 --- /dev/null +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/util/AvatarHelper.kt @@ -0,0 +1,67 @@ +package org.wordpress.android.login.util + +import android.graphics.drawable.Drawable +import android.widget.ImageView +import androidx.fragment.app.Fragment +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import org.wordpress.android.login.R +import org.wordpress.android.util.GravatarUtils +import org.wordpress.android.util.GravatarUtils.DefaultImage.STATUS_404 + +object AvatarHelper { + @JvmStatic fun loadAvatarFromEmail( + fragment: Fragment, + email: String?, + avatarView: ImageView, + listener: AvatarRequestListener + ) { + val avatarSize = fragment.resources.getDimensionPixelSize(R.dimen.avatar_sz_login) + val avatarUrl = GravatarUtils.gravatarFromEmail(email, avatarSize, STATUS_404) + loadAvatarFromUrl(fragment, avatarUrl, avatarView, listener) + } + + @JvmStatic fun loadAvatarFromUrl( + fragment: Fragment, + avatarUrl: String?, + avatarView: ImageView, + listener: AvatarRequestListener + ) { + Glide.with(fragment) + .load(avatarUrl) + .apply(RequestOptions.circleCropTransform()) + .apply(RequestOptions.placeholderOf(R.drawable.ic_user_circle_no_padding_grey_24dp)) + .apply(RequestOptions.errorOf(R.drawable.ic_user_circle_no_padding_grey_24dp)) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + listener.onRequestFinished() + return false + } + + override fun onResourceReady( + drawable: Drawable?, + model: Any?, + target: Target, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + listener.onRequestFinished() + return false + } + }) + .into(avatarView) + } + + interface AvatarRequestListener { + fun onRequestFinished() + } +} diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/util/ContextExtensions.kt b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/util/ContextExtensions.kt new file mode 100644 index 000000000000..8772eb1a4692 --- /dev/null +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/util/ContextExtensions.kt @@ -0,0 +1,29 @@ +package org.wordpress.android.login.util + +import android.content.Context +import android.content.res.ColorStateList +import android.util.TypedValue +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.ContextCompat + +@ColorRes +fun Context.getColorResIdFromAttribute(@AttrRes attribute: Int) = + TypedValue().let { + theme.resolveAttribute(attribute, it, true) + it.resourceId + } + +@ColorInt +fun Context.getColorFromAttribute(@AttrRes attribute: Int) = + TypedValue().let { + theme.resolveAttribute(attribute, it, true) + ContextCompat.getColor(this, it.resourceId) + } + +fun Context.getColorStateListFromAttribute(@AttrRes attribute: Int): ColorStateList = + getColorResIdFromAttribute(attribute).let { + AppCompatResources.getColorStateList(this, it) + } diff --git a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/widgets/WPLoginInputRow.java b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/widgets/WPLoginInputRow.java index 42f02d849428..d539017a9bfb 100644 --- a/WordPressLoginFlow/src/main/java/org/wordpress/android/login/widgets/WPLoginInputRow.java +++ b/WordPressLoginFlow/src/main/java/org/wordpress/android/login/widgets/WPLoginInputRow.java @@ -77,6 +77,13 @@ private void init(Context context, AttributeSet attrs) { mEditText.setHint(hint); // Makes the hint transparent, so the TalkBack can read it, when the field is prefilled mEditText.setHintTextColor(getResources().getColor(android.R.color.transparent)); + + // Passes autofill hints values forward to child views + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (isImportantForAutofill()) { + mEditText.setAutofillHints(getAutofillHints()); + } + } } if (a.hasValue(R.styleable.wpLoginInputRow_passwordToggleEnabled)) { mTextInputLayout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE); diff --git a/WordPressLoginFlow/src/main/res/color/login_on_background_medium_selector.xml b/WordPressLoginFlow/src/main/res/color/login_on_background_medium_selector.xml new file mode 100644 index 000000000000..bfd820bcff0f --- /dev/null +++ b/WordPressLoginFlow/src/main/res/color/login_on_background_medium_selector.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/WordPressLoginFlow/src/main/res/color/login_on_surface_high_selector.xml b/WordPressLoginFlow/src/main/res/color/login_on_surface_high_selector.xml new file mode 100644 index 000000000000..32c80592ed5a --- /dev/null +++ b/WordPressLoginFlow/src/main/res/color/login_on_surface_high_selector.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/WordPressLoginFlow/src/main/res/color/login_on_surface_medium_selector.xml b/WordPressLoginFlow/src/main/res/color/login_on_surface_medium_selector.xml new file mode 100644 index 000000000000..298e14de51c0 --- /dev/null +++ b/WordPressLoginFlow/src/main/res/color/login_on_surface_medium_selector.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/WordPressLoginFlow/src/main/res/color/material_on_surface_emphasis_low.xml b/WordPressLoginFlow/src/main/res/color/material_on_surface_emphasis_low.xml new file mode 100644 index 000000000000..bf8f29b1df0b --- /dev/null +++ b/WordPressLoginFlow/src/main/res/color/material_on_surface_emphasis_low.xml @@ -0,0 +1,4 @@ + + + + diff --git a/WordPressLoginFlow/src/main/res/drawable/ic_help_outline_white_24dp.xml b/WordPressLoginFlow/src/main/res/drawable/ic_help_outline_white_24dp.xml new file mode 100644 index 000000000000..5fdc7e34bf16 --- /dev/null +++ b/WordPressLoginFlow/src/main/res/drawable/ic_help_outline_white_24dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/WordPressLoginFlow/src/main/res/drawable/ic_user_circle_no_padding_grey_24dp.xml b/WordPressLoginFlow/src/main/res/drawable/ic_user_circle_no_padding_grey_24dp.xml new file mode 100644 index 000000000000..fe272c2b53b1 --- /dev/null +++ b/WordPressLoginFlow/src/main/res/drawable/ic_user_circle_no_padding_grey_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/WordPressLoginFlow/src/main/res/layout-land/login_magic_link_request_screen.xml b/WordPressLoginFlow/src/main/res/layout-land/login_magic_link_request_screen.xml deleted file mode 100644 index 29877bd2aff7..000000000000 --- a/WordPressLoginFlow/src/main/res/layout-land/login_magic_link_request_screen.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPressLoginFlow/src/main/res/layout-land/login_magic_link_sent_screen.xml b/WordPressLoginFlow/src/main/res/layout-land/login_magic_link_sent_screen.xml deleted file mode 100644 index ebe1186db588..000000000000 --- a/WordPressLoginFlow/src/main/res/layout-land/login_magic_link_sent_screen.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPressLoginFlow/src/main/res/layout-land/signup_bottom_sheet_dialog.xml b/WordPressLoginFlow/src/main/res/layout-land/signup_bottom_sheet_dialog.xml index a05807f5a9f8..f528b5447ef1 100644 --- a/WordPressLoginFlow/src/main/res/layout-land/signup_bottom_sheet_dialog.xml +++ b/WordPressLoginFlow/src/main/res/layout-land/signup_bottom_sheet_dialog.xml @@ -14,7 +14,7 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/WordPressLoginFlow/src/main/res/layout/login_2fa_screen.xml b/WordPressLoginFlow/src/main/res/layout/login_2fa_screen.xml index de326ff78418..b6aa8734ed4a 100644 --- a/WordPressLoginFlow/src/main/res/layout/login_2fa_screen.xml +++ b/WordPressLoginFlow/src/main/res/layout/login_2fa_screen.xml @@ -1,20 +1,17 @@ + android:paddingStart="@dimen/margin_extra_large"> @@ -23,5 +20,14 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/verification_code" - android:inputType="numberDecimal"/> + android:inputType="numberDecimal" /> + + diff --git a/WordPressLoginFlow/src/main/res/layout/login_alert_http_auth.xml b/WordPressLoginFlow/src/main/res/layout/login_alert_http_auth.xml index d9a7bba3e7b7..b9ba3cfa2994 100644 --- a/WordPressLoginFlow/src/main/res/layout/login_alert_http_auth.xml +++ b/WordPressLoginFlow/src/main/res/layout/login_alert_http_auth.xml @@ -9,7 +9,7 @@ + app:theme="@style/Widget.LoginFlow.TextInputLayout"> + app:theme="@style/Widget.LoginFlow.TextInputLayout"> diff --git a/WordPressLoginFlow/src/main/res/layout/login_email_optional_site_creds_screen.xml b/WordPressLoginFlow/src/main/res/layout/login_email_optional_site_creds_screen.xml new file mode 100644 index 000000000000..756018137097 --- /dev/null +++ b/WordPressLoginFlow/src/main/res/layout/login_email_optional_site_creds_screen.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + diff --git a/WordPressLoginFlow/src/main/res/layout/login_email_password_screen.xml b/WordPressLoginFlow/src/main/res/layout/login_email_password_screen.xml index 8053752cf6e6..5ac4d2f64fce 100644 --- a/WordPressLoginFlow/src/main/res/layout/login_email_password_screen.xml +++ b/WordPressLoginFlow/src/main/res/layout/login_email_password_screen.xml @@ -4,77 +4,66 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginBottom="@dimen/margin_extra_large" android:orientation="vertical" - android:paddingStart="@dimen/margin_extra_large" - android:paddingLeft="@dimen/margin_extra_large" android:paddingEnd="@dimen/margin_extra_large" - android:paddingRight="@dimen/margin_extra_large"> + android:paddingStart="@dimen/margin_extra_large"> - + android:layout_marginBottom="@dimen/margin_medium" + layout="@layout/login_include_email_header" /> - - - - - - - - - - - + android:layout_marginTop="@dimen/margin_extra_large" + android:visibility="gone" + tools:text="@string/enter_wpcom_password" + tools:visibility="visible" /> + + + + + + diff --git a/WordPressLoginFlow/src/main/res/layout/login_email_screen.xml b/WordPressLoginFlow/src/main/res/layout/login_email_screen.xml index 53e8e3050019..ba182e221a8c 100644 --- a/WordPressLoginFlow/src/main/res/layout/login_email_screen.xml +++ b/WordPressLoginFlow/src/main/res/layout/login_email_screen.xml @@ -1,109 +1,141 @@ - - + android:layout_height="match_parent"> - + android:layout_height="wrap_content"> - + + + + - + - + - + - + android:layout_marginBottom="@dimen/margin_small_medium" + app:layout_constraintTop_toBottomOf="@+id/spacer" + app:layout_constraintBottom_toTopOf="@+id/continue_with_google" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintVertical_bias="1.0"> - + - + - + - + + + android:layout_marginStart="@dimen/margin_extra_large" + android:layout_marginEnd="@dimen/margin_extra_large" + android:text="@string/continue_with_google_terms_of_service_text" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> - + - + diff --git a/WordPressLoginFlow/src/main/res/layout/login_email_screen_old.xml b/WordPressLoginFlow/src/main/res/layout/login_email_screen_old.xml new file mode 100644 index 000000000000..2844ae06d7b5 --- /dev/null +++ b/WordPressLoginFlow/src/main/res/layout/login_email_screen_old.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WordPressLoginFlow/src/main/res/layout/login_form_screen.xml b/WordPressLoginFlow/src/main/res/layout/login_form_screen.xml index 0621b1cc49cb..9898c35cdc60 100644 --- a/WordPressLoginFlow/src/main/res/layout/login_form_screen.xml +++ b/WordPressLoginFlow/src/main/res/layout/login_form_screen.xml @@ -8,61 +8,57 @@ android:id="@+id/toolbar_container" layout="@layout/toolbar_login" /> - + android:layout_below="@+id/toolbar_container" + android:fillViewport="true"> - - + android:layout_height="match_parent" + android:layout_marginBottom="@dimen/margin_extra_large" + android:layout_marginTop="@dimen/margin_extra_large" /> + - + android:gravity="center_vertical"> - + android:layout_marginBottom="@dimen/margin_medium_large" + android:layout_marginEnd="@dimen/margin_extra_large" + android:layout_marginStart="@dimen/margin_extra_large" + android:layout_marginTop="@dimen/margin_medium_large" + android:text="@string/login_continue" /> + diff --git a/WordPressLoginFlow/src/main/res/layout/login_include_email_header.xml b/WordPressLoginFlow/src/main/res/layout/login_include_email_header.xml new file mode 100644 index 000000000000..544cc2ea24f0 --- /dev/null +++ b/WordPressLoginFlow/src/main/res/layout/login_include_email_header.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + diff --git a/WordPressLoginFlow/src/main/res/layout/login_input_row.xml b/WordPressLoginFlow/src/main/res/layout/login_input_row.xml index d39dcbb868ea..426cdf5c2ab2 100644 --- a/WordPressLoginFlow/src/main/res/layout/login_input_row.xml +++ b/WordPressLoginFlow/src/main/res/layout/login_input_row.xml @@ -8,7 +8,7 @@ - - - - - + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingEnd="@dimen/margin_extra_large" + android:paddingStart="@dimen/margin_extra_large"> - - + - - + android:paddingEnd="@dimen/margin_none" + android:paddingStart="@dimen/margin_none" + android:text="@string/or_type_your_password" /> - + + + diff --git a/WordPressLoginFlow/src/main/res/layout/login_magic_link_sent_screen.xml b/WordPressLoginFlow/src/main/res/layout/login_magic_link_sent_screen.xml index 9597cd511515..f9192b18a9eb 100644 --- a/WordPressLoginFlow/src/main/res/layout/login_magic_link_sent_screen.xml +++ b/WordPressLoginFlow/src/main/res/layout/login_magic_link_sent_screen.xml @@ -1,44 +1,66 @@ - - - + android:layout_height="0dp" + android:layout_weight="1" + android:overScrollMode="never"> - + + + + + + + + + + + + + + android:layout_marginBottom="@dimen/margin_medium_large" + android:layout_marginEnd="@dimen/margin_extra_large" + android:layout_marginStart="@dimen/margin_extra_large" + android:layout_marginTop="@dimen/margin_medium_large" + android:text="@string/check_email" /> diff --git a/WordPressLoginFlow/src/main/res/layout/login_site_address_screen.xml b/WordPressLoginFlow/src/main/res/layout/login_site_address_screen.xml index 596cd8e6f07c..fc5c3dd0ac28 100644 --- a/WordPressLoginFlow/src/main/res/layout/login_site_address_screen.xml +++ b/WordPressLoginFlow/src/main/res/layout/login_site_address_screen.xml @@ -3,31 +3,32 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/margin_extra_large" android:orientation="vertical" - android:paddingStart="@dimen/margin_extra_large" - android:paddingLeft="@dimen/margin_extra_large" android:paddingEnd="@dimen/margin_extra_large" - android:paddingRight="@dimen/margin_extra_large"> + android:paddingStart="@dimen/margin_extra_large"> + android:inputType="textUri" /> + + diff --git a/WordPressLoginFlow/src/main/res/layout/login_username_password_screen.xml b/WordPressLoginFlow/src/main/res/layout/login_username_password_screen.xml index f2b142305cca..890485da7d78 100644 --- a/WordPressLoginFlow/src/main/res/layout/login_username_password_screen.xml +++ b/WordPressLoginFlow/src/main/res/layout/login_username_password_screen.xml @@ -4,121 +4,42 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/margin_extra_large" android:orientation="vertical" - android:paddingStart="@dimen/margin_extra_large" - android:paddingLeft="@dimen/margin_extra_large" android:paddingEnd="@dimen/margin_extra_large" - android:paddingRight="@dimen/margin_extra_large"> + android:paddingStart="@dimen/margin_extra_large"> - - - - - - - - - - - - - - - - - - - + tools:text="Enter your account information for pamelanguyen.com." /> + android:inputType="textPersonName" /> + app:passwordToggleEnabled="true" /> + + + diff --git a/WordPressLoginFlow/src/main/res/layout/signup_bottom_sheet_dialog.xml b/WordPressLoginFlow/src/main/res/layout/signup_bottom_sheet_dialog.xml index 87605487db46..d27de169e604 100644 --- a/WordPressLoginFlow/src/main/res/layout/signup_bottom_sheet_dialog.xml +++ b/WordPressLoginFlow/src/main/res/layout/signup_bottom_sheet_dialog.xml @@ -14,7 +14,7 @@ + + + + + + + + + + + + + + + + + + diff --git a/WordPressLoginFlow/src/main/res/layout/signup_email_fragment.xml b/WordPressLoginFlow/src/main/res/layout/signup_email_screen.xml similarity index 71% rename from WordPressLoginFlow/src/main/res/layout/signup_email_fragment.xml rename to WordPressLoginFlow/src/main/res/layout/signup_email_screen.xml index 50061cf17ecd..2741f5e27014 100644 --- a/WordPressLoginFlow/src/main/res/layout/signup_email_fragment.xml +++ b/WordPressLoginFlow/src/main/res/layout/signup_email_screen.xml @@ -1,23 +1,18 @@ - + android:paddingStart="@dimen/margin_extra_large"> @@ -28,7 +23,6 @@ android:hint="@string/email_address" android:imeOptions="actionNext" android:importantForAutofill="noExcludeDescendants" - android:inputType="textEmailAddress" - tools:ignore="UnusedAttribute" /> + android:inputType="textEmailAddress" /> diff --git a/WordPressLoginFlow/src/main/res/layout/signup_magic_link.xml b/WordPressLoginFlow/src/main/res/layout/signup_magic_link.xml deleted file mode 100644 index 951fa81b7e9d..000000000000 --- a/WordPressLoginFlow/src/main/res/layout/signup_magic_link.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/WordPressLoginFlow/src/main/res/layout/signup_magic_link_screen.xml b/WordPressLoginFlow/src/main/res/layout/signup_magic_link_screen.xml new file mode 100644 index 000000000000..ce723cdd451e --- /dev/null +++ b/WordPressLoginFlow/src/main/res/layout/signup_magic_link_screen.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/WordPressLoginFlow/src/main/res/layout/toolbar_login.xml b/WordPressLoginFlow/src/main/res/layout/toolbar_login.xml index 695c90671df0..6e847604bf2f 100644 --- a/WordPressLoginFlow/src/main/res/layout/toolbar_login.xml +++ b/WordPressLoginFlow/src/main/res/layout/toolbar_login.xml @@ -7,15 +7,18 @@ + android:layout_height="wrap_content"> diff --git a/WordPressLoginFlow/src/main/res/menu/menu_login.xml b/WordPressLoginFlow/src/main/res/menu/menu_login.xml index 84886dd82216..4c34dcd5fb98 100644 --- a/WordPressLoginFlow/src/main/res/menu/menu_login.xml +++ b/WordPressLoginFlow/src/main/res/menu/menu_login.xml @@ -1,11 +1,11 @@ - - + app:iconTint="?attr/colorControlNormal" + app:showAsAction="always" /> diff --git a/WordPressLoginFlow/src/main/res/values-night/colors.xml b/WordPressLoginFlow/src/main/res/values-night/colors.xml new file mode 100644 index 000000000000..3a8511ee4e51 --- /dev/null +++ b/WordPressLoginFlow/src/main/res/values-night/colors.xml @@ -0,0 +1,6 @@ + + + + #61FFFFFF + diff --git a/WordPressLoginFlow/src/main/res/values-night/themes.xml b/WordPressLoginFlow/src/main/res/values-night/themes.xml new file mode 100644 index 000000000000..f5296a5ea887 --- /dev/null +++ b/WordPressLoginFlow/src/main/res/values-night/themes.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/WordPressLoginFlow/src/main/res/values-v23/themes.xml b/WordPressLoginFlow/src/main/res/values-v23/themes.xml new file mode 100644 index 000000000000..c0fde8bb5167 --- /dev/null +++ b/WordPressLoginFlow/src/main/res/values-v23/themes.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/WordPressLoginFlow/src/main/res/values-v27/styles.xml b/WordPressLoginFlow/src/main/res/values-v27/styles.xml deleted file mode 100644 index 03a359e2ae1f..000000000000 --- a/WordPressLoginFlow/src/main/res/values-v27/styles.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/WordPressLoginFlow/src/main/res/values-v27/themes.xml b/WordPressLoginFlow/src/main/res/values-v27/themes.xml new file mode 100644 index 000000000000..fc3d18102ebf --- /dev/null +++ b/WordPressLoginFlow/src/main/res/values-v27/themes.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/WordPressLoginFlow/src/main/res/values/colors.xml b/WordPressLoginFlow/src/main/res/values/colors.xml index ec74e8753299..2412c4aca542 100644 --- a/WordPressLoginFlow/src/main/res/values/colors.xml +++ b/WordPressLoginFlow/src/main/res/values/colors.xml @@ -1,6 +1,24 @@ - + + #3582c4 + #2271b1 + #0a4b78 + + #c9356e + #8c1749 + + #d63638 + + #ffffff + #121212 + #000000 + + + #61121212 + + + - @color/status_bar - @color/login_background_color - @android:color/white + + - - @style/LoginTheme.BottomSheetDialogStyle - + - @style/LoginTheme.ToolBar + + - - - - - - + - - - - - - - - - - diff --git a/WordPressLoginFlow/src/main/res/values/themes.xml b/WordPressLoginFlow/src/main/res/values/themes.xml new file mode 100644 index 000000000000..10dc1f026ff7 --- /dev/null +++ b/WordPressLoginFlow/src/main/res/values/themes.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + diff --git a/WordPressLoginFlow/src/main/res/values/types.xml b/WordPressLoginFlow/src/main/res/values/types.xml new file mode 100644 index 000000000000..cff70fb995c7 --- /dev/null +++ b/WordPressLoginFlow/src/main/res/values/types.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372aef5e24af05341d49695ee84e5f9b594659..f3d88b1c2faf2fc91d853cd5d4242b5547257070 100644 GIT binary patch literal 58695 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 delta 48522 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�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

%A83HV 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 z \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +64,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +75,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +105,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -105,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -134,27 +154,30 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 8a0b282aa688..62bd9b9ccefe 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,14 +24,17 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +65,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +78,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line