fix auth flows
This commit is contained in:
@@ -110,6 +110,7 @@ AppLinkPolicySnapshot? appLinkPolicySnapshot(Ref ref) {
|
||||
key: _toNativeRule(value),
|
||||
},
|
||||
marketplaceFallbackEnabled: settings.appLinkMarketplaceFallback,
|
||||
authExceptionsEnabled: settings.appLinkAuthExceptionsEnabled,
|
||||
protectGeneralContext: protection.protectGeneralContext,
|
||||
protectedContextIds: protection.protectedContextIds.toList(),
|
||||
strictContextIds: protection.strictContextIds.toList(),
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ final class AppLinkPolicySnapshotProvider
|
||||
}
|
||||
|
||||
String _$appLinkPolicySnapshotHash() =>
|
||||
r'6fe2dca118d7162561fc7f6280d1a0411d50972a';
|
||||
r'4d456a3cbf091e95ac35d913e8fa20d5f25978ab';
|
||||
|
||||
/// Single serialised writer that mirrors the Dart-owned app-link policy to the
|
||||
/// native profile-scoped store (§2.8), the sole policy source consulted by the
|
||||
|
||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabRepositoryHash() => r'797520166026d1f0272cf713aa1c25ecf129c236';
|
||||
String _$tabRepositoryHash() => r'5824be32664bac24623cf8863f68b7afa254d025';
|
||||
|
||||
abstract class _$TabRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -1397,8 +1397,8 @@ final class VisibleTabListItemsFamily extends $Family
|
||||
/// switcher lays them out — the unassigned bucket first, then containers by
|
||||
/// pinned/`order_key`. Stepping off the end of one container therefore
|
||||
/// continues into the next, and selecting that tab moves the selected container
|
||||
/// along with it. Containers without tabs are skipped so their tree query never
|
||||
/// runs.
|
||||
/// along with it. Named containers holding no tabs are skipped so their tree
|
||||
/// query never runs.
|
||||
///
|
||||
/// "Previous" is a step towards the top of that order and "next" a step
|
||||
/// towards its end, so direction follows `tabListDirection` (baked into the
|
||||
@@ -1443,8 +1443,8 @@ final sequentialTabNavigationOrderProvider =
|
||||
/// switcher lays them out — the unassigned bucket first, then containers by
|
||||
/// pinned/`order_key`. Stepping off the end of one container therefore
|
||||
/// continues into the next, and selecting that tab moves the selected container
|
||||
/// along with it. Containers without tabs are skipped so their tree query never
|
||||
/// runs.
|
||||
/// along with it. Named containers holding no tabs are skipped so their tree
|
||||
/// query never runs.
|
||||
///
|
||||
/// "Previous" is a step towards the top of that order and "next" a step
|
||||
/// towards its end, so direction follows `tabListDirection` (baked into the
|
||||
@@ -1493,8 +1493,8 @@ final class SequentialTabNavigationOrderProvider
|
||||
/// switcher lays them out — the unassigned bucket first, then containers by
|
||||
/// pinned/`order_key`. Stepping off the end of one container therefore
|
||||
/// continues into the next, and selecting that tab moves the selected container
|
||||
/// along with it. Containers without tabs are skipped so their tree query never
|
||||
/// runs.
|
||||
/// along with it. Named containers holding no tabs are skipped so their tree
|
||||
/// query never runs.
|
||||
///
|
||||
/// "Previous" is a step towards the top of that order and "next" a step
|
||||
/// towards its end, so direction follows `tabListDirection` (baked into the
|
||||
|
||||
@@ -817,6 +817,11 @@ class _AppLinksModeSection extends HookConsumerWidget {
|
||||
(s) => s.appLinkMarketplaceFallback,
|
||||
),
|
||||
);
|
||||
final authExceptionsEnabled = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(s) => s.appLinkAuthExceptionsEnabled,
|
||||
),
|
||||
);
|
||||
final rules = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select((s) => s.appLinkRules),
|
||||
);
|
||||
@@ -887,6 +892,23 @@ class _AppLinksModeSection extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Allow login app callbacks'),
|
||||
subtitle: const Text(
|
||||
'Let apps that opened a Custom Tab receive their login callback, '
|
||||
'even when links are set to never open in apps',
|
||||
),
|
||||
value: authExceptionsEnabled,
|
||||
onChanged: (value) async {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(current) =>
|
||||
current.copyWith.appLinkAuthExceptionsEnabled(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
_AppLinkRulesSubsection(rules: rules),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -252,6 +252,12 @@ class GeneralSettings with FastEquatable {
|
||||
/// Defaults to false — the wrong default for a de-Googled browser.
|
||||
final bool appLinkMarketplaceFallback;
|
||||
|
||||
/// Whether app-link "never" rules allow a same-caller Custom Tab / ActionView
|
||||
/// login callback to return to the app that opened the browser. Defaults to
|
||||
/// true to keep OAuth-style sign-in flows working while normal app links still
|
||||
/// obey [appLinksMode].
|
||||
final bool appLinkAuthExceptionsEnabled;
|
||||
|
||||
/// Whether the local search index (`history` table populated via tab→
|
||||
/// history triggers) is active. When false, the SQL trigger guard returns
|
||||
/// without writing; existing rows stay until the user clears them.
|
||||
@@ -359,6 +365,7 @@ class GeneralSettings with FastEquatable {
|
||||
required this.appLinkRules,
|
||||
required this.appLinkContextOverrides,
|
||||
required this.appLinkMarketplaceFallback,
|
||||
required this.appLinkAuthExceptionsEnabled,
|
||||
required this.enableLocalSearchIndex,
|
||||
required this.indexPrivateTabs,
|
||||
required this.acceptSuggestionOnSubmit,
|
||||
@@ -436,6 +443,7 @@ class GeneralSettings with FastEquatable {
|
||||
Map<String, PersistedAppLinkRule>? appLinkRules,
|
||||
Map<String, ContextAppLinkPolicy>? appLinkContextOverrides,
|
||||
bool? appLinkMarketplaceFallback,
|
||||
bool? appLinkAuthExceptionsEnabled,
|
||||
bool? enableLocalSearchIndex,
|
||||
bool? indexPrivateTabs,
|
||||
bool? acceptSuggestionOnSubmit,
|
||||
@@ -524,6 +532,7 @@ class GeneralSettings with FastEquatable {
|
||||
appLinkRules = appLinkRules ?? const {},
|
||||
appLinkContextOverrides = appLinkContextOverrides ?? const {},
|
||||
appLinkMarketplaceFallback = appLinkMarketplaceFallback ?? false,
|
||||
appLinkAuthExceptionsEnabled = appLinkAuthExceptionsEnabled ?? true,
|
||||
enableLocalSearchIndex = enableLocalSearchIndex ?? true,
|
||||
indexPrivateTabs = indexPrivateTabs ?? false,
|
||||
acceptSuggestionOnSubmit = acceptSuggestionOnSubmit ?? true,
|
||||
@@ -693,6 +702,7 @@ class GeneralSettings with FastEquatable {
|
||||
appLinkRules,
|
||||
appLinkContextOverrides,
|
||||
appLinkMarketplaceFallback,
|
||||
appLinkAuthExceptionsEnabled,
|
||||
enableLocalSearchIndex,
|
||||
indexPrivateTabs,
|
||||
acceptSuggestionOnSubmit,
|
||||
|
||||
@@ -161,6 +161,10 @@ abstract class _$GeneralSettingsCWProxy {
|
||||
|
||||
GeneralSettings appLinkMarketplaceFallback(bool appLinkMarketplaceFallback);
|
||||
|
||||
GeneralSettings appLinkAuthExceptionsEnabled(
|
||||
bool appLinkAuthExceptionsEnabled,
|
||||
);
|
||||
|
||||
GeneralSettings enableLocalSearchIndex(bool enableLocalSearchIndex);
|
||||
|
||||
GeneralSettings indexPrivateTabs(bool indexPrivateTabs);
|
||||
@@ -253,6 +257,7 @@ abstract class _$GeneralSettingsCWProxy {
|
||||
Map<String, PersistedAppLinkRule> appLinkRules,
|
||||
Map<String, ContextAppLinkPolicy> appLinkContextOverrides,
|
||||
bool appLinkMarketplaceFallback,
|
||||
bool appLinkAuthExceptionsEnabled,
|
||||
bool enableLocalSearchIndex,
|
||||
bool indexPrivateTabs,
|
||||
bool acceptSuggestionOnSubmit,
|
||||
@@ -556,6 +561,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
||||
GeneralSettings appLinkMarketplaceFallback(bool appLinkMarketplaceFallback) =>
|
||||
call(appLinkMarketplaceFallback: appLinkMarketplaceFallback);
|
||||
|
||||
@override
|
||||
GeneralSettings appLinkAuthExceptionsEnabled(
|
||||
bool appLinkAuthExceptionsEnabled,
|
||||
) => call(appLinkAuthExceptionsEnabled: appLinkAuthExceptionsEnabled);
|
||||
|
||||
@override
|
||||
GeneralSettings enableLocalSearchIndex(bool enableLocalSearchIndex) =>
|
||||
call(enableLocalSearchIndex: enableLocalSearchIndex);
|
||||
@@ -665,6 +675,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
||||
Object? appLinkRules = const $CopyWithPlaceholder(),
|
||||
Object? appLinkContextOverrides = const $CopyWithPlaceholder(),
|
||||
Object? appLinkMarketplaceFallback = const $CopyWithPlaceholder(),
|
||||
Object? appLinkAuthExceptionsEnabled = const $CopyWithPlaceholder(),
|
||||
Object? enableLocalSearchIndex = const $CopyWithPlaceholder(),
|
||||
Object? indexPrivateTabs = const $CopyWithPlaceholder(),
|
||||
Object? acceptSuggestionOnSubmit = const $CopyWithPlaceholder(),
|
||||
@@ -1061,6 +1072,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
||||
? _value.appLinkMarketplaceFallback
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: appLinkMarketplaceFallback as bool,
|
||||
appLinkAuthExceptionsEnabled:
|
||||
appLinkAuthExceptionsEnabled == const $CopyWithPlaceholder() ||
|
||||
appLinkAuthExceptionsEnabled == null
|
||||
? _value.appLinkAuthExceptionsEnabled
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: appLinkAuthExceptionsEnabled as bool,
|
||||
enableLocalSearchIndex:
|
||||
enableLocalSearchIndex == const $CopyWithPlaceholder() ||
|
||||
enableLocalSearchIndex == null
|
||||
@@ -1257,6 +1274,7 @@ GeneralSettings _$GeneralSettingsFromJson(
|
||||
json['appLinkContextOverrides'] as Map<String, dynamic>?,
|
||||
),
|
||||
appLinkMarketplaceFallback: json['appLinkMarketplaceFallback'] as bool?,
|
||||
appLinkAuthExceptionsEnabled: json['appLinkAuthExceptionsEnabled'] as bool?,
|
||||
enableLocalSearchIndex: json['enableLocalSearchIndex'] as bool?,
|
||||
indexPrivateTabs: json['indexPrivateTabs'] as bool?,
|
||||
acceptSuggestionOnSubmit: json['acceptSuggestionOnSubmit'] as bool?,
|
||||
@@ -1356,6 +1374,7 @@ Map<String, dynamic> _$GeneralSettingsToJson(
|
||||
(k, e) => MapEntry(k, e.toJson()),
|
||||
),
|
||||
'appLinkMarketplaceFallback': instance.appLinkMarketplaceFallback,
|
||||
'appLinkAuthExceptionsEnabled': instance.appLinkAuthExceptionsEnabled,
|
||||
'enableLocalSearchIndex': instance.enableLocalSearchIndex,
|
||||
'indexPrivateTabs': instance.indexPrivateTabs,
|
||||
'acceptSuggestionOnSubmit': instance.acceptSuggestionOnSubmit,
|
||||
|
||||
@@ -109,6 +109,7 @@ const generalSettingColumnTypes = <String, DriftSqlType>{
|
||||
'customTabsEnabled': DriftSqlType.bool,
|
||||
'appLinksMode': DriftSqlType.string,
|
||||
'appLinkMarketplaceFallback': DriftSqlType.bool,
|
||||
'appLinkAuthExceptionsEnabled': DriftSqlType.bool,
|
||||
'enableLocalSearchIndex': DriftSqlType.bool,
|
||||
'indexPrivateTabs': DriftSqlType.bool,
|
||||
'acceptSuggestionOnSubmit': DriftSqlType.bool,
|
||||
|
||||
+4
@@ -19,6 +19,10 @@ class AuthIntentReceiverActivity : Activity() {
|
||||
|
||||
val sourceIntent = intent?.let { Intent(it) } ?: Intent()
|
||||
|
||||
// Stamp the caller before CustomTabIntentProcessor builds the session source, so the
|
||||
// app-links authentication carve-out can recognise a sign-in callback for this tab.
|
||||
addExternalCallerInformation(sourceIntent)
|
||||
|
||||
if (GlobalComponents.components == null && !GlobalComponents.ensureExternalComponents(applicationContext)) {
|
||||
finish()
|
||||
return
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package eu.weblibre.flutter_mozilla_components.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_CATEGORY
|
||||
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_PACKAGE
|
||||
import mozilla.components.support.utils.ext.packageManagerCompatHelper
|
||||
|
||||
/**
|
||||
* Records which app sent [intent] so the session created from it carries a `caller`.
|
||||
*
|
||||
* AC's `CustomTabIntentProcessor` reads the caller through `SafeIntent.externalPackage()`, which
|
||||
* only looks at the [EXTRA_ACTIVITY_REFERRER_PACKAGE] extra — nothing populates it for us, so a
|
||||
* receiver has to stamp it before handing the intent to the processors or every custom tab ends up
|
||||
* with `Source.External.CustomTab(null)`. Mirrors Fenix's `IntentReceiverActivity`
|
||||
* `addReferrerInformation`.
|
||||
*
|
||||
* The app-links authentication carve-out
|
||||
* ([eu.weblibre.flutter_mozilla_components.applinks.WebLibreAppLinksInterceptor]) is the consumer:
|
||||
* it lets a sign-in callback return to the app that opened the tab.
|
||||
*/
|
||||
fun Activity.addExternalCallerInformation(intent: Intent) {
|
||||
val caller = resolveExternalCallerPackage(intent) ?: return
|
||||
intent.putExtra(EXTRA_ACTIVITY_REFERRER_PACKAGE, caller)
|
||||
|
||||
// ApplicationInfo.category is API 26+; this module builds against minSdk 24.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
val category = packageManagerCompatHelper.getApplicationInfoCompat(caller, 0).category
|
||||
intent.putExtra(EXTRA_ACTIVITY_REFERRER_CATEGORY, category)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
// The caller is not resolvable — the package id alone is enough for our purposes.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort identity of the app that sent [intent].
|
||||
*
|
||||
* [Activity.getCallingPackage] is supplied by the system and cannot be forged, so it wins when
|
||||
* present (only set for `startActivityForResult` callers). The referrer chain below is
|
||||
* caller-controlled and therefore spoofable — an app can claim to be another package. Consumers
|
||||
* must not grant anything on it that the caller could not already do itself.
|
||||
*/
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
fun Activity.resolveExternalCallerPackage(intent: Intent): String? {
|
||||
callingPackage?.let { return it }
|
||||
|
||||
// Android can throw when the referrer carries data it cannot deserialise.
|
||||
val activityReferrer = try {
|
||||
referrer
|
||||
} catch (e: RuntimeException) {
|
||||
null
|
||||
}
|
||||
activityReferrer?.let { uri ->
|
||||
if (uri.scheme == ANDROID_APP_SCHEME) {
|
||||
uri.host?.let { return it }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val referrerUri: Uri? = intent.getParcelableExtra(Intent.EXTRA_REFERRER)
|
||||
if (referrerUri?.scheme == ANDROID_APP_SCHEME) {
|
||||
referrerUri.host?.let { return it }
|
||||
}
|
||||
|
||||
intent.getStringExtra(Intent.EXTRA_REFERRER_NAME)?.let { name ->
|
||||
Uri.parse(name).takeIf { it.scheme == ANDROID_APP_SCHEME }?.host?.let { return it }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private const val ANDROID_APP_SCHEME = "android-app"
|
||||
+6
-21
@@ -93,7 +93,7 @@ class IntentReceiverActivity : Activity() {
|
||||
}
|
||||
}
|
||||
|
||||
val caller = resolveCallerPackage(intent) ?: return false
|
||||
val caller = resolveExternalCallerPackage(intent) ?: return false
|
||||
if (caller == packageName) return false
|
||||
if (!IntentGatekeeperPreferences.isBlocked(applicationContext, caller)) return false
|
||||
|
||||
@@ -102,32 +102,17 @@ class IntentReceiverActivity : Activity() {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun resolveCallerPackage(intent: Intent): String? {
|
||||
referrer?.let { uri ->
|
||||
if (uri.scheme == "android-app") {
|
||||
uri.host?.let { return it }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val referrerUri: Uri? = intent.getParcelableExtra(Intent.EXTRA_REFERRER)
|
||||
if (referrerUri?.scheme == "android-app") {
|
||||
referrerUri.host?.let { return it }
|
||||
}
|
||||
|
||||
intent.getStringExtra(Intent.EXTRA_REFERRER_NAME)?.let { name ->
|
||||
Uri.parse(name).takeIf { it.scheme == "android-app" }?.host?.let { return it }
|
||||
}
|
||||
|
||||
return callingPackage
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
private fun processIntent(intent: Intent) {
|
||||
// Must run before any intent processor: CustomTabIntentProcessor reads the caller off the
|
||||
// intent when it builds the session source, and the app-links authentication carve-out
|
||||
// needs that caller to recognise a sign-in callback.
|
||||
addExternalCallerInformation(intent)
|
||||
|
||||
if (GlobalComponents.components == null) {
|
||||
if (GlobalComponents.ensureExternalComponents(applicationContext)) {
|
||||
routeIntent(intent)
|
||||
|
||||
+2
@@ -57,6 +57,7 @@ data class AppLinkPolicy(
|
||||
val protectedContextIds: Set<String>,
|
||||
val strictContextIds: Set<String>,
|
||||
val protectedTargetPatterns: List<ProtectedTargetPattern>,
|
||||
val authExceptionsEnabled: Boolean,
|
||||
/**
|
||||
* Per-container overrides keyed by contextId; only isolated containers appear. A navigation whose
|
||||
* source contextId is a key uses the entry's mode + rules instead of the global ones (replace).
|
||||
@@ -72,6 +73,7 @@ data class AppLinkPolicy(
|
||||
protectedContextIds = emptySet(),
|
||||
strictContextIds = emptySet(),
|
||||
protectedTargetPatterns = emptyList(),
|
||||
authExceptionsEnabled = true,
|
||||
contextOverrides = emptyMap(),
|
||||
)
|
||||
}
|
||||
|
||||
+10
-4
@@ -16,11 +16,15 @@ import mozilla.components.support.base.log.logger.Logger
|
||||
* behaviour so the app opens in its own recents entry.
|
||||
* - [AUTOMATIC]: global-`always` or a remembered `alwaysOpen` rule — `NEW_TASK`, subject to the
|
||||
* 2 s same-package cooldown loop-breaker (§2.4).
|
||||
* - [AUTHENTICATION]: same-caller Custom Tab / ActionView callback — `NEW_TASK | CLEAR_TOP` so the
|
||||
* originating app can resume its existing task. Not a user gesture, so it takes the same 2 s
|
||||
* cooldown as [AUTOMATIC] (AC applies its loop-breaker to authentication flows too).
|
||||
* - [MARKETPLACE]: install-app fallback — `NEW_TASK | CLEAR_TASK`.
|
||||
*/
|
||||
enum class AppLinkLaunchMode {
|
||||
MANUAL,
|
||||
AUTOMATIC,
|
||||
AUTHENTICATION,
|
||||
MARKETPLACE,
|
||||
}
|
||||
|
||||
@@ -34,9 +38,9 @@ enum class AppLinkLaunchResult {
|
||||
|
||||
/**
|
||||
* Launches external apps. Every launch re-resolves immediately first (no cache) and verifies the
|
||||
* expected package before `startActivity` (§2.7). Automatic launches honour a 2 s same-package
|
||||
* cooldown to break app→browser→app ping-pong loops (§2.4); manual and prompt-resolved opens are
|
||||
* user gestures that bypass the check but still record it.
|
||||
* expected package before `startActivity` (§2.7). Automatic and authentication launches honour a 2 s
|
||||
* same-package cooldown to break app→browser→app ping-pong loops (§2.4); manual and prompt-resolved
|
||||
* opens are user gestures that bypass the check but still record it.
|
||||
*/
|
||||
class AppLinkLauncher(
|
||||
private val resolver: ExternalAppResolver,
|
||||
@@ -81,7 +85,7 @@ class AppLinkLauncher(
|
||||
else -> resolved.packageName
|
||||
}
|
||||
|
||||
if (mode == AppLinkLaunchMode.AUTOMATIC) {
|
||||
if (mode == AppLinkLaunchMode.AUTOMATIC || mode == AppLinkLaunchMode.AUTHENTICATION) {
|
||||
val (lastPackage, lastTs) = lastLaunch
|
||||
if (lastPackage != null && lastPackage == targetPackage &&
|
||||
clock.elapsedRealtime() < lastTs + cooldownMs
|
||||
@@ -117,6 +121,8 @@ class AppLinkLauncher(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
AppLinkLaunchMode.AUTOMATIC ->
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
AppLinkLaunchMode.AUTHENTICATION ->
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
AppLinkLaunchMode.MARKETPLACE ->
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ fun AppLinkPolicySnapshot.toAppLinkPolicy(): AppLinkPolicy {
|
||||
port = pattern.port?.toInt(),
|
||||
)
|
||||
},
|
||||
authExceptionsEnabled = authExceptionsEnabled,
|
||||
contextOverrides = contextOverrides.mapValues { (_, override) ->
|
||||
ContextAppLinkPolicy(
|
||||
globalMode = override.mode.toAppLinkMode(),
|
||||
|
||||
+3
@@ -93,6 +93,7 @@ class AppLinkPolicyStore internal constructor(
|
||||
root.put(FIELD_MIGRATED, migrated)
|
||||
root.put(FIELD_GLOBAL_MODE, policy.globalMode.name)
|
||||
root.put(FIELD_MARKETPLACE, policy.marketplaceFallbackEnabled)
|
||||
root.put(FIELD_AUTH_EXCEPTIONS, policy.authExceptionsEnabled)
|
||||
root.put(FIELD_PROTECT_GENERAL, policy.protectGeneralContext)
|
||||
root.put(FIELD_PROTECTED_CONTEXTS, JSONArray(policy.protectedContextIds.toList()))
|
||||
root.put(FIELD_STRICT_CONTEXTS, JSONArray(policy.strictContextIds.toList()))
|
||||
@@ -177,6 +178,7 @@ class AppLinkPolicyStore internal constructor(
|
||||
globalMode = AppLinkMode.valueOf(root.getString(FIELD_GLOBAL_MODE)),
|
||||
rules = rules,
|
||||
marketplaceFallbackEnabled = root.optBoolean(FIELD_MARKETPLACE, false),
|
||||
authExceptionsEnabled = root.optBoolean(FIELD_AUTH_EXCEPTIONS, true),
|
||||
protectGeneralContext = root.optBoolean(FIELD_PROTECT_GENERAL, false),
|
||||
protectedContextIds = root.optJSONArray(FIELD_PROTECTED_CONTEXTS).toStringSet(),
|
||||
strictContextIds = root.optJSONArray(FIELD_STRICT_CONTEXTS).toStringSet(),
|
||||
@@ -218,6 +220,7 @@ class AppLinkPolicyStore internal constructor(
|
||||
private const val FIELD_MIGRATED = "migrated"
|
||||
private const val FIELD_GLOBAL_MODE = "globalMode"
|
||||
private const val FIELD_MARKETPLACE = "marketplaceFallbackEnabled"
|
||||
private const val FIELD_AUTH_EXCEPTIONS = "authExceptionsEnabled"
|
||||
private const val FIELD_PROTECT_GENERAL = "protectGeneralContext"
|
||||
private const val FIELD_PROTECTED_CONTEXTS = "protectedContextIds"
|
||||
private const val FIELD_STRICT_CONTEXTS = "strictContextIds"
|
||||
|
||||
+95
-11
@@ -58,9 +58,27 @@ class WebLibreAppLinksInterceptor(
|
||||
|
||||
val uriScheme = runCatching { uri.toUri().scheme }.getOrNull()
|
||||
val engineSupportsScheme = AppLinkSchemes.isEngineSupported(uriScheme)
|
||||
val session = components.core.store.state.findTabOrCustomTab(engineSession)
|
||||
val policy = AppLinkPolicyStores.forProfile(components.profileApplicationContext).policy
|
||||
|
||||
// A tab an external app opened for us (Custom Tab / ActionView) may be hosting a sign-in
|
||||
// round trip. Gated on the policy so turning the carve-out off restores the plain §2.4
|
||||
// eligibility rules rather than only skipping the launch below.
|
||||
val authExceptionsAllowed = policy.authExceptionsEnabled && isPossibleAuthentication(session)
|
||||
|
||||
// Step 2 — navigation eligibility. Any hit lets the engine proceed normally.
|
||||
if (!isEligible(uri, lastUri, uriScheme, engineSupportsScheme, hasUserGesture, isRedirect, isDirectNavigation, isSubframeRequest)) {
|
||||
if (!isEligible(
|
||||
uri,
|
||||
lastUri,
|
||||
uriScheme,
|
||||
engineSupportsScheme,
|
||||
hasUserGesture,
|
||||
isRedirect,
|
||||
isDirectNavigation,
|
||||
isSubframeRequest,
|
||||
authExceptionsAllowed,
|
||||
)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -74,10 +92,6 @@ class WebLibreAppLinksInterceptor(
|
||||
|
||||
val resolved = runtime.resolver.resolve(uri, includeHttpAppLinks = true, useCache = true)
|
||||
|
||||
val policy = AppLinkPolicyStores.forProfile(components.profileApplicationContext).policy
|
||||
|
||||
val session = components.core.store.state.findTabOrCustomTab(engineSession)
|
||||
|
||||
// Container isolation (replace semantics): a container with "isolated app link settings"
|
||||
// enabled contributes an entry keyed by its contextId. When the source tab's contextId has
|
||||
// one, its mode + rules fully replace the global ones for this navigation.
|
||||
@@ -85,12 +99,47 @@ class WebLibreAppLinksInterceptor(
|
||||
val effectiveMode = override?.globalMode ?: policy.globalMode
|
||||
val effectiveRules = override?.rules ?: policy.rules
|
||||
|
||||
val isProtectedNavigation = isProtected(policy, session, uri)
|
||||
val isPrivateNavigation = session?.content?.private ?: false
|
||||
val isWalletNavigation = AppLinkSchemes.isWallet(resolved.originalScheme) ||
|
||||
AppLinkSchemes.isWallet(resolved.intentDataScheme)
|
||||
|
||||
// An ambiguous resolution is never treated as a callback: with several handlers we cannot
|
||||
// show the caller *is* the target, and the launch would raise a chooser rather than return
|
||||
// to the app. AC declines here too — its package comes from the bound component, which is
|
||||
// only set for an unambiguous handler.
|
||||
val authTargetPackage = if (resolved.isAmbiguous) null else resolved.packageName
|
||||
|
||||
// §2.4 authentication carve-out (AC parity): a tab opened *by* the app the navigation
|
||||
// targets is a sign-in round trip rather than a general app link, so it returns to its
|
||||
// caller even under `never`. The forced-prompt contexts still win — a protected container,
|
||||
// a private tab or a wallet scheme must not leak out silently, so those fall through to the
|
||||
// classifier, which prompts for them regardless of mode (§2.4 step 4).
|
||||
if (authExceptionsAllowed &&
|
||||
isAuthenticationCallback(session, authTargetPackage) &&
|
||||
!isProtectedNavigation && !isPrivateNavigation && !isWalletNavigation
|
||||
) {
|
||||
val result = runtime.launcher.launch(
|
||||
uri,
|
||||
AppLinkLaunchMode.AUTHENTICATION,
|
||||
expectedPackage = authTargetPackage,
|
||||
)
|
||||
logger.info(
|
||||
"auth app-link callback uri=$uri tab=${session?.id} " +
|
||||
"caller=${callerPackage(session)} package=$authTargetPackage -> $result",
|
||||
)
|
||||
return if (result == AppLinkLaunchResult.LAUNCHED) {
|
||||
RequestInterceptor.InterceptionResponse.Deny
|
||||
} else {
|
||||
safeNonLaunchResponse(pendingStore, resolved)
|
||||
}
|
||||
}
|
||||
|
||||
val input = ClassifierInput(
|
||||
resolved = resolved,
|
||||
isProtected = isProtected(policy, session, uri),
|
||||
isPrivate = session?.content?.private ?: false,
|
||||
isWallet = AppLinkSchemes.isWallet(resolved.originalScheme) ||
|
||||
AppLinkSchemes.isWallet(resolved.intentDataScheme),
|
||||
isProtected = isProtectedNavigation,
|
||||
isPrivate = isPrivateNavigation,
|
||||
isWallet = isWalletNavigation,
|
||||
missingSession = session == null,
|
||||
suppressionHit = session != null &&
|
||||
pendingStore.isSuppressed(session.id, targetFingerprint(uri, resolved)),
|
||||
@@ -262,6 +311,38 @@ class WebLibreAppLinksInterceptor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when [targetPackage] is the very app that opened this tab — the shape of a sign-in
|
||||
* callback. [targetPackage] must be an unambiguous resolution; pass `null` otherwise.
|
||||
*/
|
||||
private fun isAuthenticationCallback(session: SessionState?, targetPackage: String?): Boolean {
|
||||
if (targetPackage.isNullOrEmpty()) return false
|
||||
return callerPackage(session) == targetPackage
|
||||
}
|
||||
|
||||
/**
|
||||
* The package that launched this session, as recorded by
|
||||
* [eu.weblibre.flutter_mozilla_components.activities.addExternalCallerInformation]. Note the
|
||||
* underlying referrer is caller-supplied and can be spoofed, so this may only gate actions the
|
||||
* caller could already perform itself (here: launching its own intent).
|
||||
*/
|
||||
private fun callerPackage(session: SessionState?): String? {
|
||||
return when (val source = session?.source) {
|
||||
is SessionState.Source.External.CustomTab -> source.caller?.packageId
|
||||
is SessionState.Source.External.ActionView -> source.caller?.packageId
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPossibleAuthentication(session: SessionState?): Boolean {
|
||||
return when (session?.source) {
|
||||
is SessionState.Source.External.CustomTab,
|
||||
is SessionState.Source.External.ActionView,
|
||||
-> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Eligibility (§2.4 step 2) ----
|
||||
|
||||
private fun isEligible(
|
||||
@@ -273,6 +354,7 @@ class WebLibreAppLinksInterceptor(
|
||||
isRedirect: Boolean,
|
||||
isDirectNavigation: Boolean,
|
||||
isSubframeRequest: Boolean,
|
||||
authExceptionsAllowed: Boolean,
|
||||
): Boolean {
|
||||
if (uriScheme == null) return false
|
||||
// A subframe request not triggered by the user and outside the allowlist stays in-page.
|
||||
@@ -282,8 +364,10 @@ class WebLibreAppLinksInterceptor(
|
||||
val isIntentionalNavigation = hasUserGesture || isAllowedRedirect || isDirectNavigation
|
||||
// Unintentional engine-supported navigation continues in the browser.
|
||||
if (engineSupportsScheme && !isIntentionalNavigation) return false
|
||||
// Same-domain engine-supported navigation continues in the browser (AC subdomain stripping).
|
||||
if (engineSupportsScheme && isSameDomain(lastUri, uri)) return false
|
||||
// Same-domain engine-supported navigation continues in the browser (AC subdomain stripping),
|
||||
// unless this tab could be hosting an authentication round trip whose callback is an http
|
||||
// app link on the same site.
|
||||
if (engineSupportsScheme && isSameDomain(lastUri, uri) && !authExceptionsAllowed) return false
|
||||
// Always-denied schemes never resolve or launch externally.
|
||||
if (AppLinkSchemes.isAlwaysDenied(uriScheme)) return false
|
||||
return true
|
||||
|
||||
+16
-8
@@ -5830,6 +5830,11 @@ data class AppLinkPolicySnapshot (
|
||||
/** Remembered rules keyed by canonical scope. */
|
||||
val rules: Map<String, NativeAppLinkRule>,
|
||||
val marketplaceFallbackEnabled: Boolean,
|
||||
/**
|
||||
* Allows same-caller Custom Tab / ActionView authentication callbacks to
|
||||
* return to their app even when the general app-link mode is `never`.
|
||||
*/
|
||||
val authExceptionsEnabled: Boolean,
|
||||
/** Regular / no-contextId tabs are proxied via the `general` scope. */
|
||||
val protectGeneralContext: Boolean,
|
||||
/** contextIds that resolve to a proxy after inherit/bypass/alias. */
|
||||
@@ -5850,12 +5855,13 @@ data class AppLinkPolicySnapshot (
|
||||
val globalMode = pigeonVar_list[0] as AppLinksMode
|
||||
val rules = pigeonVar_list[1] as Map<String, NativeAppLinkRule>
|
||||
val marketplaceFallbackEnabled = pigeonVar_list[2] as Boolean
|
||||
val protectGeneralContext = pigeonVar_list[3] as Boolean
|
||||
val protectedContextIds = pigeonVar_list[4] as List<String>
|
||||
val strictContextIds = pigeonVar_list[5] as List<String>
|
||||
val protectedTargetPatterns = pigeonVar_list[6] as List<ProtectedTargetPattern>
|
||||
val contextOverrides = pigeonVar_list[7] as Map<String, NativeContextAppLinkPolicy>
|
||||
return AppLinkPolicySnapshot(globalMode, rules, marketplaceFallbackEnabled, protectGeneralContext, protectedContextIds, strictContextIds, protectedTargetPatterns, contextOverrides)
|
||||
val authExceptionsEnabled = pigeonVar_list[3] as Boolean
|
||||
val protectGeneralContext = pigeonVar_list[4] as Boolean
|
||||
val protectedContextIds = pigeonVar_list[5] as List<String>
|
||||
val strictContextIds = pigeonVar_list[6] as List<String>
|
||||
val protectedTargetPatterns = pigeonVar_list[7] as List<ProtectedTargetPattern>
|
||||
val contextOverrides = pigeonVar_list[8] as Map<String, NativeContextAppLinkPolicy>
|
||||
return AppLinkPolicySnapshot(globalMode, rules, marketplaceFallbackEnabled, authExceptionsEnabled, protectGeneralContext, protectedContextIds, strictContextIds, protectedTargetPatterns, contextOverrides)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
@@ -5863,6 +5869,7 @@ data class AppLinkPolicySnapshot (
|
||||
globalMode,
|
||||
rules,
|
||||
marketplaceFallbackEnabled,
|
||||
authExceptionsEnabled,
|
||||
protectGeneralContext,
|
||||
protectedContextIds,
|
||||
strictContextIds,
|
||||
@@ -5878,7 +5885,7 @@ data class AppLinkPolicySnapshot (
|
||||
return true
|
||||
}
|
||||
val other = other as AppLinkPolicySnapshot
|
||||
return GeckoPigeonUtils.deepEquals(this.globalMode, other.globalMode) && GeckoPigeonUtils.deepEquals(this.rules, other.rules) && GeckoPigeonUtils.deepEquals(this.marketplaceFallbackEnabled, other.marketplaceFallbackEnabled) && GeckoPigeonUtils.deepEquals(this.protectGeneralContext, other.protectGeneralContext) && GeckoPigeonUtils.deepEquals(this.protectedContextIds, other.protectedContextIds) && GeckoPigeonUtils.deepEquals(this.strictContextIds, other.strictContextIds) && GeckoPigeonUtils.deepEquals(this.protectedTargetPatterns, other.protectedTargetPatterns) && GeckoPigeonUtils.deepEquals(this.contextOverrides, other.contextOverrides)
|
||||
return GeckoPigeonUtils.deepEquals(this.globalMode, other.globalMode) && GeckoPigeonUtils.deepEquals(this.rules, other.rules) && GeckoPigeonUtils.deepEquals(this.marketplaceFallbackEnabled, other.marketplaceFallbackEnabled) && GeckoPigeonUtils.deepEquals(this.authExceptionsEnabled, other.authExceptionsEnabled) && GeckoPigeonUtils.deepEquals(this.protectGeneralContext, other.protectGeneralContext) && GeckoPigeonUtils.deepEquals(this.protectedContextIds, other.protectedContextIds) && GeckoPigeonUtils.deepEquals(this.strictContextIds, other.strictContextIds) && GeckoPigeonUtils.deepEquals(this.protectedTargetPatterns, other.protectedTargetPatterns) && GeckoPigeonUtils.deepEquals(this.contextOverrides, other.contextOverrides)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@@ -5886,6 +5893,7 @@ data class AppLinkPolicySnapshot (
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.globalMode)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.rules)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.marketplaceFallbackEnabled)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.authExceptionsEnabled)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.protectGeneralContext)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.protectedContextIds)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.strictContextIds)
|
||||
@@ -5894,7 +5902,7 @@ data class AppLinkPolicySnapshot (
|
||||
return result
|
||||
}
|
||||
override fun toString(): String {
|
||||
return "AppLinkPolicySnapshot(globalMode=$globalMode, rules=$rules, marketplaceFallbackEnabled=$marketplaceFallbackEnabled, protectGeneralContext=$protectGeneralContext, protectedContextIds=$protectedContextIds, strictContextIds=$strictContextIds, protectedTargetPatterns=$protectedTargetPatterns, contextOverrides=$contextOverrides)"
|
||||
return "AppLinkPolicySnapshot(globalMode=$globalMode, rules=$rules, marketplaceFallbackEnabled=$marketplaceFallbackEnabled, authExceptionsEnabled=$authExceptionsEnabled, protectGeneralContext=$protectGeneralContext, protectedContextIds=$protectedContextIds, strictContextIds=$strictContextIds, protectedTargetPatterns=$protectedTargetPatterns, contextOverrides=$contextOverrides)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -58,6 +58,11 @@ class AppLinkClassifierTest {
|
||||
|
||||
// ---- §2.2 table: engine-supported (http) scheme, app resolves ----
|
||||
|
||||
@Test
|
||||
fun safeDefaultAllowsAuthExceptions() {
|
||||
assertEquals(true, AppLinkPolicy.SAFE_DEFAULT.authExceptionsEnabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun engineSupportedAlwaysAutoLaunches() {
|
||||
val d = AppLinkClassifier.classify(
|
||||
|
||||
+44
@@ -13,6 +13,7 @@ import kotlin.test.assertEquals
|
||||
import org.mockito.ArgumentMatchers.anyBoolean
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when`
|
||||
|
||||
class AppLinkLauncherTest {
|
||||
@@ -69,6 +70,33 @@ class AppLinkLauncherTest {
|
||||
assertEquals(1, started)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun authenticationLaunchUsesClearTopFlags() {
|
||||
val intent = mock(Intent::class.java)
|
||||
val resolved = ResolvedAppLink(
|
||||
hasExternalApp = true,
|
||||
appIntent = intent,
|
||||
packageName = "com.example.app",
|
||||
appName = "App",
|
||||
fallbackUrl = null,
|
||||
marketplaceIntent = null,
|
||||
isAmbiguous = false,
|
||||
engineSupportsScheme = false,
|
||||
scopeKey = "pkg:com.example.app",
|
||||
originalScheme = "example",
|
||||
intentDataScheme = "example",
|
||||
)
|
||||
var startedIntent: Intent? = null
|
||||
val l = launcher(resolved, FakeClock()) { startedIntent = it }
|
||||
|
||||
assertEquals(
|
||||
AppLinkLaunchResult.LAUNCHED,
|
||||
l.launch("example://callback", AppLinkLaunchMode.AUTHENTICATION),
|
||||
)
|
||||
verify(intent).flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
assertEquals(intent, startedIntent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun automaticLaunchWithinCooldownIsRefused() {
|
||||
val clock = FakeClock(1000L)
|
||||
@@ -78,6 +106,22 @@ class AppLinkLauncherTest {
|
||||
assertEquals(AppLinkLaunchResult.COOLDOWN, l.launch("zoommtg://x", AppLinkLaunchMode.AUTOMATIC))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun authenticationLaunchWithinCooldownIsRefused() {
|
||||
val clock = FakeClock(1000L)
|
||||
val l = launcher(resolvedFor("com.example.app"), clock)
|
||||
assertEquals(
|
||||
AppLinkLaunchResult.LAUNCHED,
|
||||
l.launch("zoommtg://x", AppLinkLaunchMode.AUTHENTICATION),
|
||||
)
|
||||
// An app that re-opens its Custom Tab on receiving the callback would otherwise ping-pong.
|
||||
clock.now = 1500L // < 2000 ms later
|
||||
assertEquals(
|
||||
AppLinkLaunchResult.COOLDOWN,
|
||||
l.launch("zoommtg://x", AppLinkLaunchMode.AUTHENTICATION),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun automaticLaunchAfterCooldownSucceeds() {
|
||||
val clock = FakeClock(1000L)
|
||||
|
||||
@@ -6486,6 +6486,7 @@ class AppLinkPolicySnapshot {
|
||||
required this.globalMode,
|
||||
required this.rules,
|
||||
required this.marketplaceFallbackEnabled,
|
||||
required this.authExceptionsEnabled,
|
||||
required this.protectGeneralContext,
|
||||
required this.protectedContextIds,
|
||||
required this.strictContextIds,
|
||||
@@ -6500,6 +6501,10 @@ class AppLinkPolicySnapshot {
|
||||
|
||||
bool marketplaceFallbackEnabled;
|
||||
|
||||
/// Allows same-caller Custom Tab / ActionView authentication callbacks to
|
||||
/// return to their app even when the general app-link mode is `never`.
|
||||
bool authExceptionsEnabled;
|
||||
|
||||
/// Regular / no-contextId tabs are proxied via the `general` scope.
|
||||
bool protectGeneralContext;
|
||||
|
||||
@@ -6521,6 +6526,7 @@ class AppLinkPolicySnapshot {
|
||||
globalMode,
|
||||
rules,
|
||||
marketplaceFallbackEnabled,
|
||||
authExceptionsEnabled,
|
||||
protectGeneralContext,
|
||||
protectedContextIds,
|
||||
strictContextIds,
|
||||
@@ -6540,12 +6546,13 @@ class AppLinkPolicySnapshot {
|
||||
rules: (result[1]! as Map<Object?, Object?>)
|
||||
.cast<String, NativeAppLinkRule>(),
|
||||
marketplaceFallbackEnabled: result[2]! as bool,
|
||||
protectGeneralContext: result[3]! as bool,
|
||||
protectedContextIds: (result[4]! as List<Object?>).cast<String>(),
|
||||
strictContextIds: (result[5]! as List<Object?>).cast<String>(),
|
||||
protectedTargetPatterns: (result[6]! as List<Object?>)
|
||||
authExceptionsEnabled: result[3]! as bool,
|
||||
protectGeneralContext: result[4]! as bool,
|
||||
protectedContextIds: (result[5]! as List<Object?>).cast<String>(),
|
||||
strictContextIds: (result[6]! as List<Object?>).cast<String>(),
|
||||
protectedTargetPatterns: (result[7]! as List<Object?>)
|
||||
.cast<ProtectedTargetPattern>(),
|
||||
contextOverrides: (result[7]! as Map<Object?, Object?>)
|
||||
contextOverrides: (result[8]! as Map<Object?, Object?>)
|
||||
.cast<String, NativeContextAppLinkPolicy>(),
|
||||
);
|
||||
}
|
||||
@@ -6565,6 +6572,7 @@ class AppLinkPolicySnapshot {
|
||||
marketplaceFallbackEnabled,
|
||||
other.marketplaceFallbackEnabled,
|
||||
) &&
|
||||
_deepEquals(authExceptionsEnabled, other.authExceptionsEnabled) &&
|
||||
_deepEquals(protectGeneralContext, other.protectGeneralContext) &&
|
||||
_deepEquals(protectedContextIds, other.protectedContextIds) &&
|
||||
_deepEquals(strictContextIds, other.strictContextIds) &&
|
||||
@@ -6578,7 +6586,7 @@ class AppLinkPolicySnapshot {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppLinkPolicySnapshot(globalMode: $globalMode, rules: $rules, marketplaceFallbackEnabled: $marketplaceFallbackEnabled, protectGeneralContext: $protectGeneralContext, protectedContextIds: $protectedContextIds, strictContextIds: $strictContextIds, protectedTargetPatterns: $protectedTargetPatterns, contextOverrides: $contextOverrides)';
|
||||
return 'AppLinkPolicySnapshot(globalMode: $globalMode, rules: $rules, marketplaceFallbackEnabled: $marketplaceFallbackEnabled, authExceptionsEnabled: $authExceptionsEnabled, protectGeneralContext: $protectGeneralContext, protectedContextIds: $protectedContextIds, strictContextIds: $strictContextIds, protectedTargetPatterns: $protectedTargetPatterns, contextOverrides: $contextOverrides)';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2994,6 +2994,10 @@ class AppLinkPolicySnapshot {
|
||||
|
||||
final bool marketplaceFallbackEnabled;
|
||||
|
||||
/// Allows same-caller Custom Tab / ActionView authentication callbacks to
|
||||
/// return to their app even when the general app-link mode is `never`.
|
||||
final bool authExceptionsEnabled;
|
||||
|
||||
/// Regular / no-contextId tabs are proxied via the `general` scope.
|
||||
final bool protectGeneralContext;
|
||||
|
||||
@@ -3014,6 +3018,7 @@ class AppLinkPolicySnapshot {
|
||||
required this.globalMode,
|
||||
required this.rules,
|
||||
required this.marketplaceFallbackEnabled,
|
||||
required this.authExceptionsEnabled,
|
||||
required this.protectGeneralContext,
|
||||
required this.protectedContextIds,
|
||||
required this.strictContextIds,
|
||||
|
||||
Reference in New Issue
Block a user