diff --git a/src/profileflow.cpp b/src/profileflow.cpp index 3d135e8670..72469104f5 100644 --- a/src/profileflow.cpp +++ b/src/profileflow.cpp @@ -10,6 +10,7 @@ #include "leakdetector.h" #include "logger.h" #include "mozillavpn.h" +#include "tasks/authenticate/taskauthenticate.h" #include "tasks/getsubscriptiondetails/taskgetsubscriptiondetails.h" #include "taskscheduler.h" @@ -78,10 +79,34 @@ void ProfileFlow::start() { } }); + connect(task, &TaskGetSubscriptionDetails::mustTransitionAuthToWeb, this, + [this]() { setState(StateAuthenticationNeeded); }); + TaskScheduler::scheduleTask(task); m_currentTask = task; } +void ProfileFlow::reauthenticateViaWeb() { + // We need the user to login on the browser (rather than the client), + // then we'll try getting the profile again. + TaskAuthenticate* taskAuthenticate = + new TaskAuthenticate(AuthenticationListener::AuthenticationInBrowser); + connect(taskAuthenticate, &TaskAuthenticate::authenticationAborted, this, + [this]() { + logger.debug() << "Authentication failed"; + setState(StateError); + ProfileFlow::reset(); + }); + connect( + taskAuthenticate, &TaskAuthenticate::authenticationCompleted, this, + [this]() { + logger.debug() << "Authentication succeeded, restarting profile flow"; + ProfileFlow::start(); + }); + + TaskScheduler::scheduleTask(taskAuthenticate); +} + void ProfileFlow::reset() { logger.debug() << "Reset profile flow"; diff --git a/src/profileflow.h b/src/profileflow.h index f2946e4bfb..d8fd8e39fb 100644 --- a/src/profileflow.h +++ b/src/profileflow.h @@ -20,6 +20,7 @@ class ProfileFlow final : public QObject { ~ProfileFlow(); Q_INVOKABLE void start(); + Q_INVOKABLE void reauthenticateViaWeb(); Q_INVOKABLE void reset(); enum State { diff --git a/src/tasks/getsubscriptiondetails/taskgetsubscriptiondetails.cpp b/src/tasks/getsubscriptiondetails/taskgetsubscriptiondetails.cpp index 484d70bb5e..989c0e3ceb 100644 --- a/src/tasks/getsubscriptiondetails/taskgetsubscriptiondetails.cpp +++ b/src/tasks/getsubscriptiondetails/taskgetsubscriptiondetails.cpp @@ -10,6 +10,7 @@ #include "authenticationinapp/authenticationinappsession.h" #include "authenticationlistener.h" #include "constants.h" +#include "feature/feature.h" #include "leakdetector.h" #include "logger.h" #include "models/subscriptiondata.h" @@ -126,6 +127,16 @@ void TaskGetSubscriptionDetails::initAuthentication() { logger.debug() << "Init authentication"; Q_ASSERT(!m_authenticationInAppSession); + // If transitioning from in-app auth to web-based auth, bounce to web-based +#ifndef MZ_WASM + if (!Feature::get(Feature::Feature_inAppAuthentication)->isSupported()) { + logger.info() << "Starting web-based re-authentication."; + emit mustTransitionAuthToWeb(); + emit completed(); + return; + } +#endif + emit needsAuthentication(); QByteArray pkceCodeVerifier; diff --git a/src/tasks/getsubscriptiondetails/taskgetsubscriptiondetails.h b/src/tasks/getsubscriptiondetails/taskgetsubscriptiondetails.h index 9c8cebd328..696d6bceb1 100644 --- a/src/tasks/getsubscriptiondetails/taskgetsubscriptiondetails.h +++ b/src/tasks/getsubscriptiondetails/taskgetsubscriptiondetails.h @@ -31,6 +31,7 @@ class TaskGetSubscriptionDetails final : public Task { signals: void operationCompleted(bool status); void needsAuthentication(); + void mustTransitionAuthToWeb(); private: void initAuthentication(); diff --git a/src/translations/strings.yaml b/src/translations/strings.yaml index 4e2b6d4cd8..82f32bbdb9 100644 --- a/src/translations/strings.yaml +++ b/src/translations/strings.yaml @@ -348,6 +348,8 @@ settings: appExclusionClearAllApps: value: Clear all comment: Button label to clear all apps from the exclusions list + reauthTitle: Sign In Again + reauthDescription: For your security, please sign in again to access your account information. You’ll complete this process on Mozilla’s website. localNetworkAccess: labelTitle: Exclude local network traffic diff --git a/src/ui/screens/settings/ViewSettingsMenu.qml b/src/ui/screens/settings/ViewSettingsMenu.qml index 2fab85ea65..7a53c70518 100644 --- a/src/ui/screens/settings/ViewSettingsMenu.qml +++ b/src/ui/screens/settings/ViewSettingsMenu.qml @@ -142,25 +142,29 @@ MZViewBase { VPNProfileFlow.state === VPNProfileFlow.StateReady && stackview.currentItem.objectName !== "subscriptionManagmentView" ) { + reauthPopup.close(); return stackview.push("qrc:/qt/qml/Mozilla/VPN/screens/settings/ViewSubscriptionManagement/ViewSubscriptionManagement.qml"); } // Only push the profile view if it’s not already in the stack - if ( - VPNProfileFlow.state === VPNProfileFlow.StateAuthenticationNeeded - && stackview.currentItem.objectName !== "reauthenticationFlow" - ) { + if (VPNProfileFlow.state === VPNProfileFlow.StateAuthenticationNeeded) { + if (!MZFeatureList.get("inAppAuthentication").isSupported) { + reauthPopup.open(); + } else if (stackview.currentItem.objectName !== "reauthenticationFlow") { + reauthPopup.close(); return stackview.push("qrc:/qt/qml/Mozilla/VPN/screens/settings/ViewSubscriptionManagement/ViewReauthenticationFlow.qml", { _onClose: () => { VPNProfileFlow.reset(); stackview.pop(null, StackView.Immediate); } }); + } } // An error occurred during the profile flow. Let’s reset and return // to the main settings view. const hasError = VPNProfileFlow.state === VPNProfileFlow.StateError; if (hasError) { + reauthPopup.close(); if (stackview.currentItem.objectName === "reauthenticationFlow") { stackview.pop(null, StackView.Immediate); } @@ -171,4 +175,44 @@ MZViewBase { Component.onCompleted: { Glean.impression.settingsScreen.record({screen:telemetryScreenId}); } + + MZSimplePopup { + id: reauthPopup + + anchors.centerIn: Overlay.overlay + imageSrc: "qrc:/nebula/resources/updateStatusUpdateAvailable.svg" + imageSize: Qt.size(80, 80) + title: MZI18n.SettingsReauthTitle + description: MZI18n.SettingsReauthDescription + buttons: [ + MZButton { + id: reauthButton + objectName: "reauthButton" + text: MZI18n.InAppAuthContinueToSignIn + Layout.fillWidth: true + onClicked: { + VPNProfileFlow.reauthenticateViaWeb(); + loader.state = "active"; + reauthButton.enabled = false; + } + + Rectangle { + width: MZTheme.theme.rowHeight + height: MZTheme.theme.rowHeight + anchors.right: parent.right + color: MZTheme.colors.transparent + + MZButtonLoader { + id: loader + color: MZTheme.colors.transparent + iconUrl: "qrc:/nebula/resources/spinner.svg" + } + } + } + ] + + onClosed: { + VPNProfileFlow.reset(); + } + } } diff --git a/tests/functional/queries.js b/tests/functional/queries.js index 9e1d6f8a2d..ca6b1cf6b0 100644 --- a/tests/functional/queries.js +++ b/tests/functional/queries.js @@ -280,6 +280,7 @@ const screenSettings = { STACKVIEW: new QmlQueryComposer('//settings-stackView'), APP_PREFERENCES: new QmlQueryComposer('//settingsPreferences'), USER_PROFILE: new QmlQueryComposer('//settingsUserProfile'), + REAUTH_BUTTON: new QmlQueryComposer('//reauthButton'), privacyView: { VIEW: new QmlQueryComposer('//privacySettingsView'), diff --git a/tests/functional/testSubscription.js b/tests/functional/testSubscription.js index 0c0cbc1004..f700e3ff05 100644 --- a/tests/functional/testSubscription.js +++ b/tests/functional/testSubscription.js @@ -369,7 +369,11 @@ describe('Subscription view', function() { this.ctx.resetCallbacks(); }); - it('Authentication needed - sample', async () => { + it('Authentication needed', async () => { + // With moving to in browser auth, this test no longer makes sense for WASM + if (this.ctx.wasm) { + return; + } this.ctx.fxaLoginCallback = (req) => { this.ctx.fxaOverrideEndpoints.POSTs['/v1/account/login'].body = { sessionToken: 'session', @@ -394,96 +398,15 @@ describe('Subscription view', function() { await vpn.waitForQuery(queries.screenSettings.STACKVIEW.ready()); - await vpn.waitForQuery( - queries.screenAuthenticationInApp.AUTH_SIGNIN_PASSWORD_INPUT.visible()); - await vpn.setQueryProperty( - queries.screenAuthenticationInApp.AUTH_SIGNIN_PASSWORD_INPUT.visible(), - 'text', 'P4ssw0rd!!'); - await vpn.waitForQuery( - queries.screenAuthenticationInApp.AUTH_SIGNIN_BUTTON.visible() - .enabled()); - - this.ctx.guardianSubscriptionDetailsCallback = req => { - this.ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails'] - .status = 200; - this.ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails'] - .body = SUBSCRIPTION_DETAILS; - }; - - await vpn.waitForQueryAndClick( - queries.screenAuthenticationInApp.AUTH_SIGNIN_BUTTON.visible() - .enabled()); - - await vpn.waitForQuery( - queries.screenSettings.subscriptionView.SCREEN.visible()); - }); - - it('Authentication needed - totp', async () => { - this.ctx.guardianSubscriptionDetailsCallback = req => { - this.ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails'] - .status = 401; - this.ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails'] - .body = {} - }; - - await vpn.waitForQueryAndClick(queries.navBar.SETTINGS.visible()); - await vpn.waitForQuery(queries.global.SCREEN_LOADER.ready()); - - await vpn.waitForQuery(queries.screenSettings.USER_PROFILE.visible()); - await vpn.waitForQueryAndClick( - queries.screenSettings.USER_PROFILE.visible()); - - await vpn.waitForQuery(queries.screenSettings.STACKVIEW.ready()); - - await vpn.waitForQuery( - queries.screenAuthenticationInApp.AUTH_SIGNIN_PASSWORD_INPUT.visible()); - await vpn.setQueryProperty( - queries.screenAuthenticationInApp.AUTH_SIGNIN_PASSWORD_INPUT.visible(), - 'text', 'P4ssw0rd!!'); - - await vpn.waitForQuery( - queries.screenAuthenticationInApp.AUTH_SIGNIN_BUTTON.visible() - .enabled()); - - this.ctx.fxaLoginCallback = (req) => { - this.ctx.fxaOverrideEndpoints.POSTs['/v1/account/login'].body = { - sessionToken: 'session', - verified: false, - verificationMethod: 'totp-2fa' - }; - this.ctx.fxaOverrideEndpoints.POSTs['/v1/account/login'].status = 200; - }; - - await vpn.waitForQueryAndClick( - queries.screenAuthenticationInApp.AUTH_SIGNIN_BUTTON.visible() - .enabled()); - - await vpn.waitForQuery( - queries.screenAuthenticationInApp.AUTH_TOTP_TEXT_INPUT.visible()); - await vpn.waitForQuery( - queries.screenAuthenticationInApp.AUTH_TOTP_BUTTON.visible() - .disabled()); - await vpn.setQueryProperty( - queries.screenAuthenticationInApp.AUTH_TOTP_TEXT_INPUT.visible(), - 'text', '123456'); - await vpn.waitForQuery( - queries.screenAuthenticationInApp.AUTH_TOTP_BUTTON.visible().enabled()); - - this.ctx.fxaTotpCallback = (req) => { - this.ctx.fxaOverrideEndpoints.POSTs['/v1/session/verify/totp'].body = { - success: true - } - }; - this.ctx.guardianSubscriptionDetailsCallback = req => { this.ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails'] .status = 200; this.ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails'] .body = SUBSCRIPTION_DETAILS; }; - await vpn.waitForQueryAndClick( - queries.screenAuthenticationInApp.AUTH_TOTP_BUTTON.visible().enabled()); + queries.screenSettings.REAUTH_BUTTON.visible()); + await vpn.mockInBrowserAuthentication(); await vpn.waitForQuery( queries.screenSettings.subscriptionView.SCREEN.visible());