Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
870e1b6
[MBL-19875][All] Add mobile_consent flag to auth session
hermannakos Mar 27, 2026
bec6f0d
fix tests
hermannakos Mar 27, 2026
e055f34
[MBL-19874][All] Add cookie consent dialog
hermannakos Mar 31, 2026
8f4c3de
Merge branch 'master' into MBL-19874-privacy-consent-dialog
hermannakos Mar 31, 2026
2d5f5b7
CRs
hermannakos Apr 7, 2026
cc614fe
fix tests
hermannakos Apr 7, 2026
41a0c92
Merge branch 'master' into MBL-19874-privacy-consent-dialog
tamaskozmer Apr 22, 2026
f6a266d
Conflict resolution.
tamaskozmer Apr 22, 2026
bda5b34
Fixed horizon build.
tamaskozmer Apr 22, 2026
a49a57e
Removed consent storage on the web.
tamaskozmer Apr 22, 2026
4fd0b99
Fixed oauth api call.
tamaskozmer Apr 22, 2026
b4b7b27
Restart Pendo tracking when consent is accepted in the settings.
tamaskozmer Apr 22, 2026
7f39dec
Fixed UI issues.
tamaskozmer Apr 23, 2026
2b3d6cf
Merge branch 'master' into MBL-19874-privacy-consent-dialog
tamaskozmer Apr 23, 2026
030b281
Fixed tests.
tamaskozmer Apr 23, 2026
389b546
Merge branch 'master' into MBL-19874-privacy-consent-dialog
tamaskozmer Apr 23, 2026
e04475b
Removed feature flag dependency for pandata.
tamaskozmer Apr 23, 2026
bf4230c
Correctly handle change user consent.
tamaskozmer Apr 23, 2026
025a7fa
Fixed routing to NGC.
tamaskozmer Apr 24, 2026
08166fd
Fixed QR login.
tamaskozmer Apr 24, 2026
d8271fa
New UI for changing the privacy settings.
tamaskozmer Apr 24, 2026
8305085
Fixed review items
tamaskozmer Apr 24, 2026
b604990
Changed description and title order.
tamaskozmer Apr 27, 2026
4d406d5
Fixed tests
tamaskozmer Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2026 - present Instructure, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.instructure.parentapp.di.feature

import com.instructure.canvasapi2.apis.UserAPI
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.ConsentPrefs
import com.instructure.pandautils.features.cookieconsent.AnalyticsConsentHandler
import com.instructure.pandautils.features.cookieconsent.CookieConsentNamespace
import com.instructure.pandautils.utils.FeatureFlagProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
class CookieConsentModule {

@Provides
fun provideCookieConsentNamespace(): CookieConsentNamespace {
return CookieConsentNamespace.PARENT
}

@Provides
fun provideAnalyticsConsentHandler(
userApi: UserAPI.UsersInterface,
featureFlagProvider: FeatureFlagProvider,
consentPrefs: ConsentPrefs,
apiPrefs: ApiPrefs
): AnalyticsConsentHandler {
return object : AnalyticsConsentHandler(userApi, featureFlagProvider, consentPrefs, apiPrefs) {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ import com.instructure.canvasapi2.utils.Analytics
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.loginapi.login.LoginNavigation
import com.instructure.loginapi.login.features.acceptableusepolicy.AcceptableUsePolicyRouter
import com.instructure.loginapi.login.features.cookieconsent.CookieConsentRouter
import com.instructure.loginapi.login.util.LoginPrefs
import com.instructure.pandautils.features.reminder.AlarmScheduler
import com.instructure.parentapp.features.login.ParentAcceptableUsePolicyRouter
import com.instructure.parentapp.features.login.ParentCookieConsentRouter
import com.instructure.parentapp.features.login.ParentLoginNavigation
import com.instructure.parentapp.features.login.SignInActivity
import dagger.Module
Expand Down Expand Up @@ -59,6 +61,11 @@ class LoginModule {
): LoginNavigation {
return ParentLoginNavigation(activity, alarmScheduler)
}

@Provides
fun provideCookieConsentRouter(activity: FragmentActivity): CookieConsentRouter {
return ParentCookieConsentRouter(activity)
}
}

@Module
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ import androidx.fragment.app.FragmentActivity
import com.instructure.canvasapi2.utils.Analytics
import com.instructure.canvasapi2.utils.AnalyticsEventConstants
import com.instructure.loginapi.login.features.acceptableusepolicy.AcceptableUsePolicyRouter
import com.instructure.loginapi.login.features.cookieconsent.CookieConsentActivity
import com.instructure.loginapi.login.tasks.LogoutTask
import com.instructure.pandautils.features.reminder.AlarmScheduler
import com.instructure.parentapp.R
import com.instructure.parentapp.features.main.MainActivity
import com.instructure.parentapp.features.webview.HtmlContentActivity
import com.instructure.parentapp.util.ParentLogoutTask

Expand All @@ -42,8 +42,9 @@ class ParentAcceptableUsePolicyRouter(
override fun startApp() {
CookieManager.getInstance().flush()

val intent = Intent(activity, MainActivity::class.java)
val intent = Intent(activity, CookieConsentActivity::class.java)
activity.intent?.extras?.let { intent.putExtras(it) }
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
activity.startActivity(intent)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2026 - present Instructure, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.instructure.parentapp.features.login

import android.content.Intent
import androidx.fragment.app.FragmentActivity
import com.instructure.loginapi.login.features.cookieconsent.CookieConsentRouter
import com.instructure.parentapp.features.main.MainActivity

class ParentCookieConsentRouter(
private val activity: FragmentActivity
) : CookieConsentRouter {

override fun startApp() {
val intent = Intent(activity, MainActivity::class.java)
activity.intent?.extras?.let { intent.putExtras(it) }
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
activity.startActivity(intent)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ sealed class RouteValidatorAction {
data class ShowToast(val message: String) : RouteValidatorAction()
data class StartSignInActivity(val accountDomain: AccountDomain) : RouteValidatorAction()
data object StartLoginActivity : RouteValidatorAction()
data object StartCookieConsent : RouteValidatorAction()
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.instructure.pandautils.binding.viewBinding
import com.instructure.pandautils.utils.Const
import com.instructure.pandautils.utils.collectOneOffEvents
import com.instructure.parentapp.databinding.ActivityRouteValidatorBinding
import com.instructure.loginapi.login.features.cookieconsent.CookieConsentActivity
import com.instructure.parentapp.features.login.LoginActivity
import com.instructure.parentapp.features.login.SignInActivity
import com.instructure.parentapp.features.main.MainActivity
Expand Down Expand Up @@ -60,6 +61,7 @@ class RouteValidatorActivity : BaseCanvasActivity() {
is RouteValidatorAction.ShowToast -> Toast.makeText(this, action.message, Toast.LENGTH_LONG).show()
is RouteValidatorAction.StartSignInActivity -> startSignInActivity(action.accountDomain)
is RouteValidatorAction.StartLoginActivity -> startLoginActivity()
is RouteValidatorAction.StartCookieConsent -> startCookieConsent()
}
}

Expand Down Expand Up @@ -92,6 +94,13 @@ class RouteValidatorActivity : BaseCanvasActivity() {
finish()
}

private fun startCookieConsent() {
val intent = Intent(this, CookieConsentActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
}

companion object {
fun createIntent(context: Context, uri: Uri): Intent {
val intent = Intent(context, RouteValidatorActivity::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class RouteValidatorViewModel @Inject constructor(
} else {
// Log the analytics - only for real logins, not masquerading
logQREvent(apiPrefs.domain, true)
postActionWithDelay(RouteValidatorAction.StartMainActivity())
postActionWithDelay(RouteValidatorAction.StartCookieConsent)
}
return@tryLaunch
} catch (e: Throwable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ParentSettingsBehaviour(private val selectedStudentHolder: SelectedStudent
get() = mapOf(
R.string.preferences to listOf(SettingsItem.APP_THEME),
R.string.inboxSettingsTitle to listOf(SettingsItem.INBOX_SIGNATURE),
R.string.legal to listOf(SettingsItem.ABOUT, SettingsItem.LEGAL)
R.string.legal to listOf(SettingsItem.ABOUT, SettingsItem.LEGAL, SettingsItem.PRIVACY)
)

override suspend fun applyAppSpecificColorSettings() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ class ParentSettingsRouter(private val navigation: Navigation, private val activ
override fun navigateToInboxSignature() {
navigation.navigate(activity, navigation.inboxSignatureSettings)
}

override fun navigateToPrivacySettings() {
navigation.navigate(activity, navigation.privacySettings)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.ConsentPrefs
import com.instructure.canvasapi2.utils.weave.catch
import com.instructure.canvasapi2.utils.weave.tryLaunch
import com.instructure.pandautils.utils.ColorKeeper
Expand All @@ -43,6 +44,7 @@ class SplashViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val repository: SplashRepository,
private val apiPrefs: ApiPrefs,
private val consentPrefs: ConsentPrefs,
private val colorKeeper: ColorKeeper,
private val featureFlagProvider: FeatureFlagProvider,
savedStateHandle: SavedStateHandle
Expand Down Expand Up @@ -83,18 +85,20 @@ class SplashViewModel @Inject constructor(
}
}

val sendUsageMetrics = repository.getSendUsageMetrics()
if (sendUsageMetrics) {
val userWithIds = repository.getSelfWithUuid()
val visitorData = mapOf(
"locale" to apiPrefs.effectiveLocale,
)
val accountData = mapOf(
"surveyOptOut" to featureFlagProvider.checkAccountSurveyNotificationsFlag()
)
Pendo.startSession(userWithIds?.uuid?.SHA256().orEmpty(), userWithIds?.accountUuid.orEmpty(), visitorData, accountData)
} else {
Pendo.endSession()
if (consentPrefs.currentUserConsent == true) {
val sendUsageMetrics = repository.getSendUsageMetrics()
if (sendUsageMetrics) {
val userWithIds = repository.getSelfWithUuid()
val visitorData = mapOf(
"locale" to apiPrefs.effectiveLocale,
)
val accountData = mapOf(
"surveyOptOut" to featureFlagProvider.checkAccountSurveyNotificationsFlag()
)
Pendo.startSession(userWithIds?.uuid?.SHA256().orEmpty(), userWithIds?.accountUuid.orEmpty(), visitorData, accountData)
} else {
Pendo.endSession()
}
}

val students = repository.getStudents()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.instructure.pandautils.features.inbox.utils.InboxComposeOptions
import com.instructure.pandautils.features.lti.LtiLaunchFragment
import com.instructure.pandautils.features.settings.SettingsFragment
import com.instructure.pandautils.features.settings.inboxsignature.InboxSignatureFragment
import com.instructure.pandautils.features.privacysettings.PrivacySettingsFragment
import com.instructure.pandautils.utils.Const
import com.instructure.pandautils.utils.fromJson
import com.instructure.pandautils.utils.toJson
Expand Down Expand Up @@ -80,6 +81,7 @@ class Navigation(apiPrefs: ApiPrefs) {
val qrPairing = "$baseUrl/qr-pairing"
val settings = "$baseUrl/settings"
val inboxSignatureSettings = "$baseUrl/inboxSignatureSettings"
val privacySettings = "$baseUrl/privacySettings"

private fun splashRoute(qrCodeMasqueradeId: Long) = "$baseUrl/splash/$qrCodeMasqueradeId"
fun assignmentDetailsRoute(courseId: Long, assignmentId: Long) = "$baseUrl/courses/${courseId}/assignments/${assignmentId}"
Expand Down Expand Up @@ -280,6 +282,7 @@ class Navigation(apiPrefs: ApiPrefs) {
}
}
fragment<InboxSignatureFragment>(inboxSignatureSettings)
fragment<PrivacySettingsFragment>(privacySettings)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.instructure.canvasapi2.models.CanvasColor
import com.instructure.canvasapi2.models.CanvasTheme
import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.ConsentPrefs
import com.instructure.canvasapi2.utils.ContextKeeper
import com.instructure.pandautils.utils.ColorKeeper
import com.instructure.pandautils.utils.Const
Expand Down Expand Up @@ -67,6 +68,7 @@ class SplashViewModelTest {
private val context: Context = mockk(relaxed = true)
private val repository: SplashRepository = mockk(relaxed = true)
private val apiPrefs: ApiPrefs = mockk(relaxed = true)
private val consentPrefs: ConsentPrefs = mockk(relaxed = true)
private val colorKeeper: ColorKeeper = mockk(relaxed = true)
private val savedStateHandle = mockk<SavedStateHandle>(relaxed = true)
private val featureFlagProvider = mockk<FeatureFlagProvider>(relaxed = true)
Expand Down Expand Up @@ -262,6 +264,7 @@ class SplashViewModelTest {
@Test
fun `Send usage metrics enabled`() = runTest {
coEvery { repository.getSendUsageMetrics() } returns true
every { consentPrefs.currentUserConsent } returns true

createViewModel()

Expand All @@ -275,6 +278,7 @@ class SplashViewModelTest {
@Test
fun `Send usage metrics disabled`() = runTest {
coEvery { repository.getSendUsageMetrics() } returns false
every { consentPrefs.currentUserConsent } returns true

createViewModel()

Expand All @@ -290,6 +294,7 @@ class SplashViewModelTest {
context = context,
repository = repository,
apiPrefs = apiPrefs,
consentPrefs = consentPrefs,
colorKeeper = colorKeeper,
featureFlagProvider = featureFlagProvider,
savedStateHandle = savedStateHandle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.APIHelper
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.ApiType
import com.instructure.canvasapi2.utils.ConsentPrefs
import com.instructure.canvasapi2.utils.LinkHeaders
import com.instructure.canvasapi2.utils.Logger
import com.instructure.canvasapi2.utils.RemoteConfigParam
Expand Down Expand Up @@ -109,6 +110,9 @@ abstract class CallbackActivity : ParentActivity(), OnUnreadCountInvalidated, No
@Inject
lateinit var apiPrefs: ApiPrefs

@Inject
lateinit var consentPrefs: ConsentPrefs

@Inject
lateinit var courseApi: CourseAPI.CoursesInterface

Expand Down Expand Up @@ -214,6 +218,8 @@ abstract class CallbackActivity : ParentActivity(), OnUnreadCountInvalidated, No
}

private suspend fun setupPendoTracking() {
if (consentPrefs.currentUserConsent != true) return

val user = userApi.getSelfWithUUID(RestParams(isForceReadFromNetwork = true)).dataOrNull
val featureFlagsResult = FeaturesManager.getEnvironmentFeatureFlagsAsync(true).await().dataOrNull
val sendUsageMetrics = featureFlagsResult?.get(FeaturesManager.SEND_USAGE_METRICS) ?: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.instructure.canvasapi2.utils.weave.apiAsync
import com.instructure.canvasapi2.utils.weave.catch
import com.instructure.canvasapi2.utils.weave.tryWeave
import com.instructure.horizon.HorizonActivity
import com.instructure.loginapi.login.features.cookieconsent.CookieConsentActivity
import com.instructure.loginapi.login.tasks.LogoutTask
import com.instructure.loginapi.login.util.QRLogin.performSSOLogin
import com.instructure.loginapi.login.util.QRLogin.verifySSOLoginUri
Expand Down Expand Up @@ -164,7 +165,7 @@ class InterwebsToApplication : BaseCanvasActivity() {
} else {
// Log the analytics - only for real logins, not masquerading
logQREvent(ApiPrefs.domain, true)
Intent(this@InterwebsToApplication, NavigationActivity.startActivityClass)
Intent(this@InterwebsToApplication, CookieConsentActivity::class.java)
}

intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ import com.instructure.canvasapi2.models.AccountDomain
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.loginapi.login.LoginNavigation
import com.instructure.loginapi.login.features.acceptableusepolicy.AcceptableUsePolicyRouter
import com.instructure.loginapi.login.features.cookieconsent.CookieConsentRouter
import com.instructure.loginapi.login.util.LoginPrefs
import com.instructure.pandautils.features.reminder.AlarmScheduler
import com.instructure.pandautils.room.offline.DatabaseProvider
import com.instructure.student.activity.SignInActivity
import com.instructure.student.features.login.StudentAcceptableUsePolicyRouter
import com.instructure.student.features.login.StudentCookieConsentRouter
import com.instructure.student.features.login.StudentLoginNavigation
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -59,6 +61,13 @@ class LoginModule {
): LoginNavigation {
return StudentLoginNavigation(activity, databaseProvider, alarmScheduler)
}

@Provides
fun provideCookieConsentRouter(
activity: FragmentActivity
): CookieConsentRouter {
return StudentCookieConsentRouter(activity)
}
}

@Module
Expand Down
Loading
Loading