diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt
index 395cf7ec5..e7053f2c6 100644
--- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt
+++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt
@@ -34,6 +34,7 @@ import org.openedx.core.system.notifier.app.AppUpgradeEvent
import org.openedx.core.system.notifier.app.SignInEvent
import org.openedx.core.utils.Logger
import org.openedx.foundation.presentation.BaseViewModel
+import org.openedx.foundation.presentation.UIMessage
import org.openedx.foundation.system.ResourceManager
import org.openedx.core.R as CoreRes
@@ -217,7 +218,23 @@ class SignInViewModel(
interactor.loginSocial(token, authType)
}.onFailure { error ->
logger.e { "Social login error: $error" }
- onUnknownError()
+ if (error is EdxError.InvalidGrantException) {
+ // The social identity resolved on Google's side but is not linked to any
+ // account on this platform. The mobile token-exchange endpoint only signs in
+ // existing/linked users, so guide the user to register (mirrors iOS/web).
+ sendMessage(
+ UIMessage.SnackBarMessage(
+ resourceManager.getString(
+ R.string.auth_social_account_not_registered,
+ authType.methodName,
+ configuration.getPlatformName(),
+ )
+ )
+ )
+ _uiState.update { it.copy(showProgress = false) }
+ } else {
+ onUnknownError()
+ }
}.onSuccess {
logger.d { "Social login (${authType.methodName}) success" }
_uiState.update { it.copy(loginSuccess = true) }
diff --git a/auth/src/main/java/org/openedx/auth/presentation/sso/GoogleAuthHelper.kt b/auth/src/main/java/org/openedx/auth/presentation/sso/GoogleAuthHelper.kt
index 99985b882..c5e2ced6e 100644
--- a/auth/src/main/java/org/openedx/auth/presentation/sso/GoogleAuthHelper.kt
+++ b/auth/src/main/java/org/openedx/auth/presentation/sso/GoogleAuthHelper.kt
@@ -105,6 +105,20 @@ class GoogleAuthHelper(private val config: Config) {
private companion object {
const val TAG = "GoogleAuthHelper"
- const val SCOPE = "oauth2: https://www.googleapis.com/auth/userinfo.email"
+
+ /**
+ * Scopes requested for the Google access token that is exchanged on the backend via
+ * `/oauth2/exchange_access_token/google-oauth2/`.
+ *
+ * Must match the scopes expected by the server-side social-auth `google-oauth2` backend
+ * (`DEFAULT_SCOPE = ["openid", "email", "profile"]`) and the iOS/web clients. Requesting
+ * only `userinfo.email` yields a token whose `userinfo` response lacks the profile/name
+ * claims, so first-time SSO account creation fails and the exchange returns
+ * `invalid_grant` ("access_token is not valid"). The `oauth2:` prefix is immediately
+ * followed by a space-separated scope list (no leading space).
+ */
+ const val SCOPE = "oauth2:openid " +
+ "https://www.googleapis.com/auth/userinfo.email " +
+ "https://www.googleapis.com/auth/userinfo.profile"
}
}
diff --git a/auth/src/main/res/values/strings.xml b/auth/src/main/res/values/strings.xml
index 77401c27f..9838dab6b 100644
--- a/auth/src/main/res/values/strings.xml
+++ b/auth/src/main/res/values/strings.xml
@@ -10,6 +10,7 @@
Email or Username
Invalid email or username
Password is too short
+ This %1$s account is not linked with any %2$s account. Please register.
Welcome back! Sign in to access your courses.
Show optional fields
Hide optional fields