diff --git a/.github/workflows/action-pr-approved.yaml b/.github/workflows/action-pr-approved.yaml
deleted file mode 100644
index d9289e66cad8..000000000000
--- a/.github/workflows/action-pr-approved.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: Pull Request Reviewed
-
-on:
- pull_request_review:
- types: [submitted]
-
-jobs:
- pr-reviewed:
- if: github.event.review.state == 'approved'
- runs-on: ubuntu-latest
- steps:
- - name: Update Asana task -> PR approved
- uses: duckduckgo/native-github-asana-sync@v1.1
- with:
- asana-pat: ${{ secrets.GH_ASANA_SECRET }}
- trigger-phrase: "Task/Issue URL:"
- action: 'notify-pr-approved'
\ No newline at end of file
diff --git a/.github/workflows/pr-review-notifications.yaml b/.github/workflows/pr-review-notifications.yaml
index fc2c2b9d745c..e39c117c962f 100644
--- a/.github/workflows/pr-review-notifications.yaml
+++ b/.github/workflows/pr-review-notifications.yaml
@@ -1,4 +1,4 @@
-name: Pull Request Reviewed -> Asana Sync
+name: Pull Request Reviewed -> Sync With Asana
on:
pull_request_review:
@@ -6,8 +6,8 @@ on:
jobs:
pr-reviewed:
- name: Update Asana task -> PR reviewed
- uses: duckduckgo/native-github-asana-sync/.github/workflows/pr-review-notifications.yml@david/improve-pr-notifications
+ name: Add PR reviewed comment
+ uses: duckduckgo/native-github-asana-sync/.github/workflows/pr-review-notifications.yml@v1.4.1
with:
trigger-phrase: "Task/Issue URL:"
secrets:
diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml
index 734ba3cc6bc5..32ea3646c177 100644
--- a/app/lint-baseline.xml
+++ b/app/lint-baseline.xml
@@ -7679,4 +7679,15 @@
column="9"/>
+
+
+
+
diff --git a/app/src/androidTest/resources/reference_tests/domain_matching_tests.json b/app/src/androidTest/resources/reference_tests/domain_matching_tests.json
index 6186e1181f02..22b32afcfc65 100644
--- a/app/src/androidTest/resources/reference_tests/domain_matching_tests.json
+++ b/app/src/androidTest/resources/reference_tests/domain_matching_tests.json
@@ -34,6 +34,13 @@
"requestType": "script",
"expectAction": "ignore"
},
+ {
+ "name": "same party ignore with deeper subdomain",
+ "siteURL": "https://bad.etld-plus-two.site/",
+ "requestURL": "https://bad.etld-plus-two.site/script.js",
+ "requestType": "script",
+ "expectAction": "ignore"
+ },
{
"name": "tracker loads ignore",
"siteURL": "https://bad.third-party.site/",
diff --git a/app/src/androidTest/resources/reference_tests/tracker_radar_reference.json b/app/src/androidTest/resources/reference_tests/tracker_radar_reference.json
index 9a831c9e5063..aff7c7750124 100644
--- a/app/src/androidTest/resources/reference_tests/tracker_radar_reference.json
+++ b/app/src/androidTest/resources/reference_tests/tracker_radar_reference.json
@@ -255,6 +255,21 @@
"rules": [],
"default": "ignore"
},
+ "bad.etld-plus-two.site": {
+ "domain": "bad.etld-plus-two.site",
+ "owner": {
+ "name": "Test Site for Tracker Blocking With eTLD+2",
+ "displayName": "Bad Third Party Site eTLD+2",
+ "privacyPolicy": "",
+ "url": "http://bad.etld-plus-two.site"
+ },
+ "prevalence": 0.1,
+ "fingerprinting": 3,
+ "cookies": 0.1,
+ "categories": [],
+ "default": "block",
+ "rules": []
+ },
"tracker.test": {
"domain": "tracker.test",
"owner": {
@@ -819,6 +834,13 @@
"prevalence": 0.1,
"displayName": "Test Site for Tracker Blocking"
},
+ "Test Site for Tracker Blocking With eTLD+2": {
+ "domains": [
+ "bad.etld-plus-two.site"
+ ],
+ "prevalence": 0.1,
+ "displayName": "Bad Third Party Site eTLD+2"
+ },
"Tests for formatting": {
"domains": [
"format.test"
@@ -876,6 +898,7 @@
"bad.third-party.site": "Test Site for Tracker Blocking",
"sometimes-bad.third-party.site": "Test Site for Tracker Blocking",
"broken.third-party.site": "Test Site for Tracker Blocking",
+ "bad.etld-plus-two.site": "Test Site for Tracker Blocking With eTLD+2",
"format.test": "Tests for formatting",
"third-party.site": "Test Site for Tracker Blocking",
"tracker.test": "Test Site for Tracker Blocking",
diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
index b04da52c6bdb..53cb2e137eda 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
@@ -31,6 +31,7 @@ import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.VisibleForTesting
+import androidx.core.view.isVisible
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.MarginPageTransformer
@@ -186,7 +187,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
wasSwipingStarted = false
viewModel.onTabsSwiped()
- tabManager.tabPagerAdapter.onPageChanged(position)
+ tabManager.onTabPageSwiped(position)
}
}
@@ -234,6 +235,9 @@ open class BrowserActivity : DuckDuckGoActivity() {
}
setContentView(binding.root)
+
+ initializeTabs()
+
viewModel.viewState.observe(this) {
renderer.renderBrowserViewState(it)
}
@@ -420,7 +424,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
val existingTabId = intent.getStringExtra(OPEN_EXISTING_TAB_ID_EXTRA)
if (existingTabId != null) {
if (swipingTabsFeature.isEnabled) {
- tabManager.openExistingTab(existingTabId)
+ tabManager.switchToTab(existingTabId)
} else {
openExistingTab(existingTabId)
}
@@ -442,7 +446,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
} else {
Timber.w("can't use current tab, opening in new tab instead")
if (swipingTabsFeature.isEnabled) {
- tabManager.openInNewTab(query = sharedText, skipHome = true)
+ tabManager.launchNewTab(query = sharedText, skipHome = true)
} else {
lifecycleScope.launch { viewModel.onOpenInNewTabRequested(query = sharedText, skipHome = true) }
}
@@ -459,7 +463,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
val sourceTabId = if (selectedText) currentTab?.tabId else null
val skipHome = !selectedText
if (swipingTabsFeature.isEnabled) {
- tabManager.openInNewTab(sourceTabId = sourceTabId, query = sharedText, skipHome = skipHome)
+ tabManager.launchNewTab(sourceTabId = sourceTabId, query = sharedText, skipHome = skipHome)
} else {
lifecycleScope.launch { viewModel.onOpenInNewTabRequested(sourceTabId = sourceTabId, query = sharedText, skipHome = skipHome) }
}
@@ -480,7 +484,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
if (swipingTabsFeature.isEnabled) {
lifecycleScope.launch {
viewModel.tabsFlow.flowWithLifecycle(lifecycle).collectLatest {
- tabManager.onTabsUpdated(it)
+ tabManager.onTabsChanged(it)
}
}
@@ -562,8 +566,8 @@ open class BrowserActivity : DuckDuckGoActivity() {
is Command.ShowAppRatingPrompt -> showAppRatingDialog(command.promptCount)
is Command.ShowAppFeedbackPrompt -> showGiveFeedbackDialog(command.promptCount)
is Command.LaunchFeedbackView -> startActivity(FeedbackActivity.intent(this))
- is Command.SwitchToTab -> tabManager.openExistingTab(command.tabId)
- is Command.OpenInNewTab -> tabManager.openInNewTab(command.url)
+ is Command.SwitchToTab -> tabManager.switchToTab(command.tabId)
+ is Command.OpenInNewTab -> tabManager.launchNewTab(command.url)
is Command.OpenSavedSite -> currentTab?.submitQuery(command.url)
}
}
@@ -777,19 +781,15 @@ open class BrowserActivity : DuckDuckGoActivity() {
}
}
- @SuppressLint("ClickableViewAccessibility", "WrongConstant")
private fun initializeTabs() {
if (swipingTabsFeature.isEnabled) {
tabPager.adapter = tabManager.tabPagerAdapter
tabPager.registerOnPageChangeCallback(onTabPageChangeListener)
tabPager.setPageTransformer(MarginPageTransformer(resources.getDimension(com.duckduckgo.mobile.android.R.dimen.keyline_2).toPx().toInt()))
-
- binding.fragmentContainer.gone()
- tabPager.show()
- } else {
- binding.fragmentContainer.show()
- tabPager.gone()
}
+
+ binding.fragmentContainer.isVisible = !swipingTabsFeature.isEnabled
+ tabPager.isVisible = swipingTabsFeature.isEnabled
}
private val Intent.launchedFromRecents: Boolean
diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
index 429cc1e980bc..aa567c8b14bc 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
@@ -835,10 +835,9 @@ class BrowserTabFragment :
}
}
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
omnibar = Omnibar(settingsDataStore.omnibarPosition, changeOmnibarPositionFeature.refactor().isEnabled(), binding)
webViewContainer = binding.webViewContainer
@@ -1076,6 +1075,9 @@ class BrowserTabFragment :
onMenuItemClicked(changeBrowserModeMenuItem) {
viewModel.onChangeBrowserModeClicked()
}
+ onMenuItemClicked(defaultBrowserMenuItem) {
+ viewModel.onSetDefaultBrowserSelected()
+ }
onMenuItemClicked(sharePageMenuItem) {
pixel.fire(AppPixelName.MENU_ACTION_SHARE_PRESSED)
viewModel.onShareSelected()
@@ -1530,7 +1532,7 @@ class BrowserTabFragment :
is NavigationCommand.Refresh -> refresh()
is Command.OpenInNewTab -> {
if (swipingTabsFeature.isEnabled) {
- requireBrowserActivity().tabManager.openInNewTab(it.query, it.sourceTabId)
+ requireBrowserActivity().tabManager.launchNewTab(it.query, it.sourceTabId)
} else {
browserActivity?.openInNewTab(it.query, it.sourceTabId)
}
@@ -1808,7 +1810,7 @@ class BrowserTabFragment :
binding.autoCompleteSuggestionsList.gone()
if (swipingTabsFeature.isEnabled) {
- requireBrowserActivity().tabManager.openExistingTab(it.tabId)
+ requireBrowserActivity().tabManager.switchToTab(it.tabId)
} else {
browserActivity?.openExistingTab(it.tabId)
}
@@ -3017,15 +3019,17 @@ class BrowserTabFragment :
}
override fun onContextItemSelected(item: MenuItem): Boolean {
- runCatching {
- webView?.safeHitTestResult?.let {
- val target = getLongPressTarget(it)
- if (target != null && viewModel.userSelectedItemFromLongPressMenu(target, item)) {
- return true
+ if (this.isResumed) {
+ runCatching {
+ webView?.safeHitTestResult?.let {
+ val target = getLongPressTarget(it)
+ if (target != null && viewModel.userSelectedItemFromLongPressMenu(target, item)) {
+ return true
+ }
}
+ }.onFailure { exception ->
+ Timber.e(exception, "Failed to get HitTestResult")
}
- }.onFailure { exception ->
- Timber.e(exception, "Failed to get HitTestResult")
}
return super.onContextItemSelected(item)
}
diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
index 84540ecce5bd..3fe751887c2c 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
@@ -2525,6 +2525,10 @@ class BrowserTabViewModel @Inject constructor(
}
}
+ fun onSetDefaultBrowserSelected() {
+ // no-op, to be implemented
+ }
+
fun onShareSelected() {
url?.let {
viewModelScope.launch(dispatchers.io()) {
diff --git a/app/src/main/java/com/duckduckgo/app/browser/menu/BrowserPopupMenu.kt b/app/src/main/java/com/duckduckgo/app/browser/menu/BrowserPopupMenu.kt
index b955233543cb..d62cf953ca03 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/menu/BrowserPopupMenu.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/menu/BrowserPopupMenu.kt
@@ -87,6 +87,13 @@ class BrowserPopupMenu(
}
}
+ internal val defaultBrowserMenuItem: View by lazy {
+ when (omnibarPosition) {
+ TOP -> topBinding.includeDefaultBrowserMenuItem.defaultBrowserMenuItem
+ BOTTOM -> bottomBinding.includeDefaultBrowserMenuItem.defaultBrowserMenuItem
+ }
+ }
+
internal val sharePageMenuItem: View by lazy {
when (omnibarPosition) {
TOP -> topBinding.sharePageMenuItem
@@ -240,6 +247,8 @@ class BrowserPopupMenu(
newTabMenuItem.isVisible = browserShowing && !displayedInCustomTabScreen
sharePageMenuItem.isVisible = viewState.canSharePage
+ defaultBrowserMenuItem.isVisible = viewState.showSelectDefaultBrowserMenuItem
+
bookmarksMenuItem.isVisible = !displayedInCustomTabScreen
downloadsMenuItem.isVisible = !displayedInCustomTabScreen
settingsMenuItem.isVisible = !displayedInCustomTabScreen
diff --git a/app/src/main/java/com/duckduckgo/app/browser/tabs/DefaultTabManager.kt b/app/src/main/java/com/duckduckgo/app/browser/tabs/DefaultTabManager.kt
index 9f8d2c298aab..04d9336d4d2d 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/tabs/DefaultTabManager.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/tabs/DefaultTabManager.kt
@@ -20,130 +20,83 @@ import android.os.Message
import androidx.lifecycle.lifecycleScope
import com.duckduckgo.app.browser.BrowserActivity
import com.duckduckgo.app.browser.BrowserTabFragment
-import com.duckduckgo.app.browser.SkipUrlConversionOnNewTabFeature
-import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter
import com.duckduckgo.app.browser.tabs.adapter.TabPagerAdapter
import com.duckduckgo.app.tabs.model.TabEntity
-import com.duckduckgo.app.tabs.model.TabRepository
-import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import dagger.android.DaggerActivity
import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.transformWhile
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
-import timber.log.Timber
+
+interface TabManager : CoroutineScope {
+ companion object {
+ const val MAX_ACTIVE_TABS = 20
+ }
+
+ val tabOperationManager: TabOperationManager
+
+ val currentTab: BrowserTabFragment?
+ val tabPagerAdapter: TabPagerAdapter
+
+ fun onTabPageSwiped(newPosition: Int)
+ fun openMessageInNewTab(message: Message, sourceTabId: String?)
+ fun clearTabsInMemory()
+ fun onCleanup()
+
+ fun onSelectedTabChanged(tabId: String) = tabOperationManager.onSelectedTabChanged(tabId)
+ fun onTabsChanged(updatedTabIds: List) = launch { tabOperationManager.onTabsChanged(updatedTabIds) }
+ fun switchToTab(tabId: String) = launch { tabOperationManager.selectTab(tabId) }
+ fun launchNewTab(query: String? = null, sourceTabId: String? = null, skipHome: Boolean = false) = launch {
+ tabOperationManager.openNewTab(query, sourceTabId, skipHome)
+ }
+}
@ContributesBinding(ActivityScope::class)
@SingleInstanceIn(ActivityScope::class)
class DefaultTabManager @Inject constructor(
activity: DaggerActivity,
- private val tabRepository: TabRepository,
- private val dispatchers: DispatcherProvider,
- private val queryUrlConverter: OmnibarEntryConverter,
- private val skipUrlConversionOnNewTabFeature: SkipUrlConversionOnNewTabFeature,
+ override val tabOperationManager: TabOperationManager,
) : TabManager {
private val browserActivity = activity as BrowserActivity
private val supportFragmentManager = activity.supportFragmentManager
- private var openMessageInNewTabJob: Job? = null
-
- private val keepSingleTab: Boolean
- get() = !browserActivity.tabPager.isUserInputEnabled
-
private val coroutineScope = browserActivity.lifecycleScope
- var selectedTabId: String? = null
- private set
+ private var openMessageInNewTabJob: Job? = null
override val tabPagerAdapter by lazy {
TabPagerAdapter(
fragmentManager = supportFragmentManager,
lifecycleOwner = browserActivity,
activityIntent = browserActivity.intent,
- getSelectedTabId = { selectedTabId },
+ getSelectedTabId = { tabOperationManager.getSelectedTabId() },
getTabById = ::getTabById,
- requestNewTab = ::requestNewTab,
- onTabSelected = { tabId -> openExistingTab(tabId) },
+ requestAndWaitForNewTab = ::requestAndWaitForNewTab,
)
}
- override val currentTab: BrowserTabFragment?
- get() = tabPagerAdapter.currentFragment
-
- override fun onSelectedTabChanged(tabId: String) {
- Timber.d("### TabManager.onSelectedTabChanged: $tabId")
- selectedTabId = tabId
-
- if (keepSingleTab) {
- tabPagerAdapter.onTabsUpdated(listOf(tabId))
- }
+ init {
+ tabOperationManager.registerCallbacks(::onTabsUpdated, ::shouldKeepSingleTab)
}
- override fun onTabsUpdated(updatedTabIds: List) {
- Timber.d("### TabManager.onTabsUpdated: $updatedTabIds")
- if (keepSingleTab) {
- updatedTabIds.firstOrNull { it == selectedTabId }?.let {
- tabPagerAdapter.onTabsUpdated(listOf(it))
- }
- } else {
- tabPagerAdapter.onTabsUpdated(updatedTabIds)
- }
-
- if (updatedTabIds.isEmpty()) {
- coroutineScope.launch(dispatchers.io()) {
- Timber.i("Tabs list is null or empty; adding default tab")
- tabRepository.addDefaultTab()
- }
+ override fun onTabPageSwiped(newPosition: Int) {
+ val tabId = tabPagerAdapter.getTabIdAtPosition(newPosition)
+ if (tabId != null) {
+ switchToTab(tabId)
}
}
+ override val currentTab: BrowserTabFragment?
+ get() = tabPagerAdapter.currentFragment
+
override fun openMessageInNewTab(message: Message, sourceTabId: String?) {
openMessageInNewTabJob = coroutineScope.launch {
tabPagerAdapter.setMessageForNewFragment(message)
- openNewTab(sourceTabId)
- }
- }
-
- override fun openExistingTab(tabId: String) {
- coroutineScope.launch(dispatchers.io()) {
- if (tabId != tabRepository.getSelectedTab()?.tabId) {
- tabRepository.select(tabId)
- }
- }
- }
-
- override fun launchNewTab() {
- coroutineScope.launch { openNewTab() }
- }
-
- override fun openInNewTab(
- query: String,
- sourceTabId: String?,
- skipHome: Boolean,
- ) {
- coroutineScope.launch {
- val url = if (skipUrlConversionOnNewTabFeature.self().isEnabled()) {
- query
- } else {
- queryUrlConverter.convertQueryToUrl(query)
- }
-
- if (sourceTabId != null) {
- tabRepository.addFromSourceTab(
- url = url,
- skipHome = skipHome,
- sourceTabId = sourceTabId,
- )
- } else {
- tabRepository.add(
- url = url,
- skipHome = skipHome,
- )
- }
+ tabOperationManager.openNewTab(sourceTabId)
}
}
@@ -155,26 +108,19 @@ class DefaultTabManager @Inject constructor(
openMessageInNewTabJob?.cancel()
}
- private suspend fun openNewTab(sourceTabId: String? = null): String {
- return if (sourceTabId != null) {
- tabRepository.addFromSourceTab(sourceTabId = sourceTabId)
- } else {
- tabRepository.add()
- }
+ private fun onTabsUpdated(updatedTabIds: List) {
+ tabPagerAdapter.onTabsUpdated(updatedTabIds)
}
- private fun requestNewTab(): TabEntity = runBlocking(dispatchers.io()) {
- val tabId = openNewTab()
- return@runBlocking tabRepository.flowTabs.transformWhile { result ->
- result.firstOrNull { it.tabId == tabId }?.let { entity ->
- emit(entity)
- return@transformWhile true
- }
- return@transformWhile false
- }.first()
- }
+ private fun shouldKeepSingleTab() = !browserActivity.tabPager.isUserInputEnabled
private fun getTabById(tabId: String): TabEntity? = runBlocking {
- tabRepository.getTab(tabId)
+ return@runBlocking tabOperationManager.getTabById(tabId)
+ }
+
+ private fun requestAndWaitForNewTab(): TabEntity = runBlocking {
+ return@runBlocking tabOperationManager.requestAndWaitForNewTab()
}
+
+ override val coroutineContext: CoroutineContext = activity.lifecycleScope.coroutineContext
}
diff --git a/app/src/main/java/com/duckduckgo/app/browser/tabs/DefaultTabOperationManager.kt b/app/src/main/java/com/duckduckgo/app/browser/tabs/DefaultTabOperationManager.kt
new file mode 100644
index 000000000000..ea40c1697335
--- /dev/null
+++ b/app/src/main/java/com/duckduckgo/app/browser/tabs/DefaultTabOperationManager.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2024 DuckDuckGo
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.duckduckgo.app.browser.tabs
+
+import com.duckduckgo.app.browser.SkipUrlConversionOnNewTabFeature
+import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter
+import com.duckduckgo.app.tabs.model.TabEntity
+import com.duckduckgo.app.tabs.model.TabRepository
+import com.duckduckgo.common.utils.DispatcherProvider
+import com.duckduckgo.di.scopes.ActivityScope
+import com.squareup.anvil.annotations.ContributesBinding
+import javax.inject.Inject
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.transformWhile
+import kotlinx.coroutines.withContext
+import timber.log.Timber
+
+interface TabOperationManager {
+ fun registerCallbacks(onTabsUpdated: (List) -> Unit, shouldKeepSingleTab: () -> Boolean)
+ fun getSelectedTabId(): String?
+ fun onSelectedTabChanged(tabId: String)
+
+ suspend fun onTabsChanged(updatedTabIds: List)
+ suspend fun selectTab(tabId: String)
+ suspend fun requestAndWaitForNewTab(): TabEntity
+ suspend fun openNewTab(query: String? = null, sourceTabId: String? = null, skipHome: Boolean = false): String
+ suspend fun getTabById(tabId: String): TabEntity?
+}
+
+@ContributesBinding(ActivityScope::class)
+class DefaultTabOperationManager @Inject constructor(
+ private val tabRepository: TabRepository,
+ private val dispatchers: DispatcherProvider,
+ private val queryUrlConverter: OmnibarEntryConverter,
+ private val skipUrlConversionOnNewTabFeature: SkipUrlConversionOnNewTabFeature,
+) : TabOperationManager {
+ private lateinit var onTabsUpdated: (List) -> Unit
+ private lateinit var shouldKeepSingleTab: () -> Boolean
+ private var selectedTabId: String? = null
+
+ override fun registerCallbacks(onTabsUpdated: (List) -> Unit, shouldKeepSingleTab: () -> Boolean) {
+ this.onTabsUpdated = onTabsUpdated
+ this.shouldKeepSingleTab = shouldKeepSingleTab
+ }
+
+ override fun getSelectedTabId(): String? = selectedTabId
+
+ override fun onSelectedTabChanged(tabId: String) {
+ Timber.d("### TabManager.onSelectedTabChanged: $tabId")
+ selectedTabId = tabId
+
+ if (shouldKeepSingleTab()) {
+ onTabsUpdated(listOf(tabId))
+ }
+ }
+
+ override suspend fun onTabsChanged(updatedTabIds: List) {
+ Timber.d("### TabManager.onTabsUpdated: $updatedTabIds")
+ if (shouldKeepSingleTab()) {
+ updatedTabIds.firstOrNull { it == selectedTabId }?.let {
+ onTabsUpdated(listOf(it))
+ }
+ } else {
+ onTabsUpdated(updatedTabIds)
+ }
+
+ if (updatedTabIds.isEmpty()) {
+ withContext(dispatchers.io()) {
+ Timber.i("Tabs list is null or empty; adding default tab")
+ tabRepository.addDefaultTab()
+ }
+ }
+ }
+
+ override suspend fun requestAndWaitForNewTab(): TabEntity = withContext(dispatchers.io()) {
+ val tabId = openNewTab()
+ return@withContext tabRepository.flowTabs.transformWhile { result ->
+ result.firstOrNull { it.tabId == tabId }?.let { entity ->
+ emit(entity)
+ return@transformWhile true
+ }
+ return@transformWhile false
+ }.first()
+ }
+
+ override suspend fun selectTab(tabId: String) = withContext(dispatchers.io()) {
+ if (tabId != tabRepository.getSelectedTab()?.tabId) {
+ tabRepository.select(tabId)
+ }
+ }
+
+ override suspend fun openNewTab(
+ query: String?,
+ sourceTabId: String?,
+ skipHome: Boolean,
+ ): String = withContext(dispatchers.io()) {
+ val url = query?.let {
+ if (skipUrlConversionOnNewTabFeature.self().isEnabled()) {
+ query
+ } else {
+ queryUrlConverter.convertQueryToUrl(query)
+ }
+ }
+
+ return@withContext if (sourceTabId != null) {
+ tabRepository.addFromSourceTab(
+ url = url,
+ skipHome = skipHome,
+ sourceTabId = sourceTabId,
+ )
+ } else {
+ tabRepository.add(
+ url = url,
+ skipHome = skipHome,
+ )
+ }
+ }
+
+ override suspend fun getTabById(tabId: String): TabEntity? = withContext(dispatchers.io()) {
+ return@withContext tabRepository.getTab(tabId)
+ }
+}
diff --git a/app/src/main/java/com/duckduckgo/app/browser/tabs/TabManager.kt b/app/src/main/java/com/duckduckgo/app/browser/tabs/TabManager.kt
deleted file mode 100644
index 76606c7a0ee8..000000000000
--- a/app/src/main/java/com/duckduckgo/app/browser/tabs/TabManager.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (c) 2024 DuckDuckGo
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.duckduckgo.app.browser.tabs
-
-import android.os.Message
-import com.duckduckgo.app.browser.BrowserTabFragment
-import com.duckduckgo.app.browser.tabs.adapter.TabPagerAdapter
-
-interface TabManager {
- companion object {
- const val MAX_ACTIVE_TABS = 20
- }
-
- val currentTab: BrowserTabFragment?
- val tabPagerAdapter: TabPagerAdapter
-
- fun onSelectedTabChanged(tabId: String)
- fun onTabsUpdated(updatedTabIds: List)
-
- fun openMessageInNewTab(message: Message, sourceTabId: String?)
- fun openExistingTab(tabId: String)
- fun launchNewTab()
- fun openInNewTab(query: String, sourceTabId: String? = null, skipHome: Boolean = false)
-
- fun clearTabsInMemory()
- fun onCleanup()
-}
diff --git a/app/src/main/java/com/duckduckgo/app/browser/tabs/adapter/TabPagerAdapter.kt b/app/src/main/java/com/duckduckgo/app/browser/tabs/adapter/TabPagerAdapter.kt
index 09c146a0f869..485bae7f8618 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/tabs/adapter/TabPagerAdapter.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/tabs/adapter/TabPagerAdapter.kt
@@ -32,9 +32,8 @@ class TabPagerAdapter(
private val fragmentManager: FragmentManager,
private val activityIntent: Intent?,
private val getTabById: (String) -> TabEntity?,
- private val requestNewTab: () -> TabEntity,
+ private val requestAndWaitForNewTab: () -> TabEntity,
private val getSelectedTabId: () -> String?,
- private val onTabSelected: (String) -> Unit,
) : FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle) {
private val tabIds = mutableListOf()
private var messageForNewFragment: Message? = null
@@ -51,7 +50,7 @@ class TabPagerAdapter(
.firstOrNull { it.tabId == getSelectedTabId() }
override fun createFragment(position: Int): Fragment {
- val tab = getTabById(tabIds[position]) ?: requestNewTab()
+ val tab = getTabById(tabIds[position]) ?: requestAndWaitForNewTab()
val isExternal = activityIntent?.getBooleanExtra(BrowserActivity.LAUNCH_FROM_EXTERNAL_EXTRA, false) == true
return if (messageForNewFragment != null) {
@@ -77,9 +76,11 @@ class TabPagerAdapter(
tabIds.addAll(newTabs)
}
- fun onPageChanged(position: Int) {
- if (position < tabIds.size) {
- onTabSelected(tabIds[position])
+ fun getTabIdAtPosition(position: Int): String? {
+ return if (position < tabIds.size) {
+ tabIds[position]
+ } else {
+ null
}
}
diff --git a/app/src/main/java/com/duckduckgo/app/browser/viewstate/BrowserViewState.kt b/app/src/main/java/com/duckduckgo/app/browser/viewstate/BrowserViewState.kt
index 66dd8df17782..bfb487f1e751 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/viewstate/BrowserViewState.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/viewstate/BrowserViewState.kt
@@ -34,6 +34,7 @@ data class BrowserViewState(
val showTabsButton: Boolean = true,
val fireButton: HighlightableButton = HighlightableButton.Visible(),
val showMenuButton: HighlightableButton = HighlightableButton.Visible(),
+ val showSelectDefaultBrowserMenuItem: Boolean = false,
val canSharePage: Boolean = false,
val canSaveSite: Boolean = false,
val bookmark: SavedSite.Bookmark? = null,
diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt
index 429c4902c5c2..54c6c7c5e661 100644
--- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt
+++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt
@@ -139,40 +139,41 @@ class CtaViewModel @Inject constructor(
}
suspend fun onCtaShown(cta: Cta) {
- cta.shownPixel?.let {
- val canSendPixel = when (cta) {
- is DaxCta -> cta.canSendShownPixel()
- else -> true
+ withContext(dispatchers.io()) {
+ cta.shownPixel?.let {
+ val canSendPixel = when (cta) {
+ is DaxCta -> cta.canSendShownPixel()
+ else -> true
+ }
+ if (canSendPixel) {
+ pixel.fire(it, cta.pixelShownParameters())
+ }
}
- if (canSendPixel) {
- pixel.fire(it, cta.pixelShownParameters())
+ if (cta is OnboardingDaxDialogCta && cta.markAsReadOnShow) {
+ dismissedCtaDao.insert(DismissedCta(cta.ctaId))
}
- }
- if (cta is OnboardingDaxDialogCta && cta.markAsReadOnShow) {
- dismissedCtaDao.insert(DismissedCta(cta.ctaId))
- }
- if (cta is BrokenSitePromptDialogCta) {
- brokenSitePrompt.ctaShown()
- }
- withContext(dispatchers.io()) {
+ if (cta is BrokenSitePromptDialogCta) {
+ brokenSitePrompt.ctaShown()
+ }
+
if (cta is DaxBubbleCta.DaxPrivacyProCta || cta is DaxBubbleCta.DaxExperimentPrivacyProCta) {
extendedOnboardingPixelsPlugin.testPrivacyProOnboardingShownMetricPixel()?.getPixelDefinitions()?.forEach {
pixel.fire(it.pixelName, it.params)
}
}
- }
- // Temporary pixel
- val isVisitSiteSuggestionsCta =
- cta is DaxBubbleCta.DaxIntroVisitSiteOptionsCta || cta is DaxBubbleCta.DaxExperimentIntroVisitSiteOptionsCta ||
- cta is OnboardingDaxDialogCta.DaxSiteSuggestionsCta || cta is OnboardingDaxDialogCta.DaxExperimentSiteSuggestionsCta
- if (isVisitSiteSuggestionsCta) {
- if (userBrowserProperties.daysSinceInstalled() <= MIN_DAYS_TO_COUNT_ONBOARDING_CTA_SHOWN) {
- val count = onboardingStore.visitSiteCtaDisplayCount ?: 0
- pixel.fire(AppPixelName.ONBOARDING_VISIT_SITE_CTA_SHOWN, mapOf("count" to count.toString()))
- onboardingStore.visitSiteCtaDisplayCount = count + 1
- } else {
- onboardingStore.clearVisitSiteCtaDisplayCount()
+ // Temporary pixel
+ val isVisitSiteSuggestionsCta =
+ cta is DaxBubbleCta.DaxIntroVisitSiteOptionsCta || cta is DaxBubbleCta.DaxExperimentIntroVisitSiteOptionsCta ||
+ cta is OnboardingDaxDialogCta.DaxSiteSuggestionsCta || cta is OnboardingDaxDialogCta.DaxExperimentSiteSuggestionsCta
+ if (isVisitSiteSuggestionsCta) {
+ if (userBrowserProperties.daysSinceInstalled() <= MIN_DAYS_TO_COUNT_ONBOARDING_CTA_SHOWN) {
+ val count = onboardingStore.visitSiteCtaDisplayCount ?: 0
+ pixel.fire(AppPixelName.ONBOARDING_VISIT_SITE_CTA_SHOWN, mapOf("count" to count.toString()))
+ onboardingStore.visitSiteCtaDisplayCount = count + 1
+ } else {
+ onboardingStore.clearVisitSiteCtaDisplayCount()
+ }
}
}
}
@@ -250,8 +251,8 @@ class CtaViewModel @Inject constructor(
}
suspend fun getFireDialogCta(): OnboardingDaxDialogCta? {
- if (!daxOnboardingActive() || daxDialogFireEducationShown()) return null
return withContext(dispatchers.io()) {
+ if (!daxOnboardingActive() || daxDialogFireEducationShown()) return@withContext null
if (highlightsOnboardingExperimentManager.isHighlightsEnabled()) {
return@withContext OnboardingDaxDialogCta.DaxExperimentFireButtonCta(onboardingStore, appInstallStore)
} else {
@@ -261,8 +262,8 @@ class CtaViewModel @Inject constructor(
}
suspend fun getSiteSuggestionsDialogCta(): OnboardingDaxDialogCta? {
- if (!daxOnboardingActive() || !canShowDaxIntroVisitSiteCta()) return null
return withContext(dispatchers.io()) {
+ if (!daxOnboardingActive() || !canShowDaxIntroVisitSiteCta()) return@withContext null
if (highlightsOnboardingExperimentManager.isHighlightsEnabled()) {
return@withContext OnboardingDaxDialogCta.DaxExperimentSiteSuggestionsCta(onboardingStore, appInstallStore)
} else {
@@ -272,8 +273,8 @@ class CtaViewModel @Inject constructor(
}
suspend fun getEndStaticDialogCta(): OnboardingDaxDialogCta.DaxExperimentEndStaticCta? {
- if (!daxOnboardingActive() && daxDialogEndShown()) return null
return withContext(dispatchers.io()) {
+ if (!daxOnboardingActive() && daxDialogEndShown()) return@withContext null
return@withContext OnboardingDaxDialogCta.DaxExperimentEndStaticCta(onboardingStore, appInstallStore)
}
}
@@ -473,7 +474,7 @@ class CtaViewModel @Inject constructor(
}
}
- private suspend fun isSiteNotAllowedForOnboarding(site: Site): Boolean {
+ private fun isSiteNotAllowedForOnboarding(site: Site): Boolean {
val uri = site.url.toUri()
if (subscriptions.isPrivacyProUrl(uri)) return true
@@ -502,9 +503,11 @@ class CtaViewModel @Inject constructor(
// We only want to show New Tab when the Home CTAs from Onboarding has finished
// https://app.asana.com/0/1157893581871903/1207769731595075/f
suspend fun areBubbleDaxDialogsCompleted(): Boolean {
- val noBrowserCtaExperiment = extendedOnboardingFeatureToggles.noBrowserCtas().isEnabled()
- val bubbleCtasShown = daxDialogEndShown() && (daxDialogNetworkShown() || daxDialogOtherShown() || daxDialogTrackersFoundShown())
- return noBrowserCtaExperiment || bubbleCtasShown || hideTips() || !userStageStore.daxOnboardingActive()
+ return withContext(dispatchers.io()) {
+ val noBrowserCtaExperiment = extendedOnboardingFeatureToggles.noBrowserCtas().isEnabled()
+ val bubbleCtasShown = daxDialogEndShown() && (daxDialogNetworkShown() || daxDialogOtherShown() || daxDialogTrackersFoundShown())
+ noBrowserCtaExperiment || bubbleCtasShown || hideTips() || !userStageStore.daxOnboardingActive()
+ }
}
private fun daxDialogSerpShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_DIALOG_SERP)
diff --git a/app/src/main/res/drawable/background_default_browser_menu_item.xml b/app/src/main/res/drawable/background_default_browser_menu_item.xml
new file mode 100644
index 000000000000..2b7583e3949c
--- /dev/null
+++ b/app/src/main/res/drawable/background_default_browser_menu_item.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/popup_window_browser_menu.xml b/app/src/main/res/layout/popup_window_browser_menu.xml
index 255f393c7760..98a6a4907ce5 100644
--- a/app/src/main/res/layout/popup_window_browser_menu.xml
+++ b/app/src/main/res/layout/popup_window_browser_menu.xml
@@ -97,6 +97,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt
index 5430f65e7a59..77c3eb510824 100644
--- a/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt
+++ b/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt
@@ -39,6 +39,7 @@ import com.duckduckgo.app.tabs.model.TabRepository
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
import com.duckduckgo.feature.toggles.api.Toggle.State
+import com.duckduckgo.tabs.model.TabDataRepositoryTest.Companion.TAB_ID
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.After
diff --git a/app/src/test/java/com/duckduckgo/app/cta/ui/OnboardingDaxDialogTests.kt b/app/src/test/java/com/duckduckgo/app/cta/ui/OnboardingDaxDialogTests.kt
index 2bdf27b047be..020ded0aeaa5 100644
--- a/app/src/test/java/com/duckduckgo/app/cta/ui/OnboardingDaxDialogTests.kt
+++ b/app/src/test/java/com/duckduckgo/app/cta/ui/OnboardingDaxDialogTests.kt
@@ -38,7 +38,6 @@ import com.duckduckgo.app.widget.ui.WidgetCapabilities
import com.duckduckgo.brokensite.api.BrokenSitePrompt
import com.duckduckgo.browser.api.UserBrowserProperties
import com.duckduckgo.common.test.CoroutineTestRule
-import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.feature.toggles.api.Toggle
import kotlinx.coroutines.test.runTest
@@ -72,7 +71,6 @@ class OnboardingDaxDialogTests {
private val onboardingStore: OnboardingStore = mock()
private val userStageStore: UserStageStore = mock()
private val tabRepository: TabRepository = mock()
- private val dispatchers: DispatcherProvider = mock()
private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector = mock()
private val extendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles = mock()
private val mockDuckPlayer: DuckPlayer = mock()
@@ -95,7 +93,13 @@ class OnboardingDaxDialogTests {
widgetCapabilities,
dismissedCtaDao,
userAllowListRepository,
- settingsDataStore, onboardingStore, userStageStore, tabRepository, dispatchers, duckDuckGoUrlDetector, extendedOnboardingFeatureToggles,
+ settingsDataStore,
+ onboardingStore,
+ userStageStore,
+ tabRepository,
+ coroutineRule.testDispatcherProvider,
+ duckDuckGoUrlDetector,
+ extendedOnboardingFeatureToggles,
subscriptions = mock(),
mockDuckPlayer,
mockHighlightsOnboardingExperimentManager,
diff --git a/common/common-ui/src/main/res/drawable/ic_default_browser_mobile_color_16.xml b/common/common-ui/src/main/res/drawable/ic_default_browser_mobile_color_16.xml
new file mode 100644
index 000000000000..8e8bd4b949ff
--- /dev/null
+++ b/common/common-ui/src/main/res/drawable/ic_default_browser_mobile_color_16.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/network-protection/network-protection-api/src/main/java/com/duckduckgo/networkprotection/api/NetworkProtectionState.kt b/network-protection/network-protection-api/src/main/java/com/duckduckgo/networkprotection/api/NetworkProtectionState.kt
index 1c67dee66dde..2119466a3fb4 100644
--- a/network-protection/network-protection-api/src/main/java/com/duckduckgo/networkprotection/api/NetworkProtectionState.kt
+++ b/network-protection/network-protection-api/src/main/java/com/duckduckgo/networkprotection/api/NetworkProtectionState.kt
@@ -91,5 +91,8 @@ interface NetworkProtectionState {
CONNECTED,
CONNECTING,
DISCONNECTED,
+ ;
+
+ fun isConnected(): Boolean = this == CONNECTED
}
}
diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionManager.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionManager.kt
index 643b764de7d5..7d45824807f6 100644
--- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionManager.kt
+++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionManager.kt
@@ -19,6 +19,7 @@ package com.duckduckgo.networkprotection.impl.subscription
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus
+import com.duckduckgo.settings.api.NewSettingsFeature
import com.duckduckgo.subscriptions.api.Product.NetP
import com.duckduckgo.subscriptions.api.SubscriptionStatus
import com.duckduckgo.subscriptions.api.Subscriptions
@@ -37,6 +38,8 @@ interface NetpSubscriptionManager {
EXPIRED,
SIGNED_OUT,
INACTIVE,
+ WAITING,
+ INELIGIBLE,
}
}
@@ -52,6 +55,7 @@ fun VpnStatus.isExpired(): Boolean {
class RealNetpSubscriptionManager @Inject constructor(
private val subscriptions: Subscriptions,
private val dispatcherProvider: DispatcherProvider,
+ private val newSettingsFeature: NewSettingsFeature,
) : NetpSubscriptionManager {
override suspend fun getVpnStatus(): VpnStatus {
@@ -71,15 +75,29 @@ class RealNetpSubscriptionManager @Inject constructor(
private fun hasValidEntitlementFlow(): Flow = subscriptions.getEntitlementStatus().map { it.contains(NetP) }
private suspend fun getVpnStatusInternal(hasValidEntitlement: Boolean): VpnStatus {
- val subscriptionState = subscriptions.getSubscriptionStatus()
- return when (subscriptionState) {
- SubscriptionStatus.INACTIVE, SubscriptionStatus.EXPIRED -> VpnStatus.EXPIRED
- SubscriptionStatus.UNKNOWN -> VpnStatus.SIGNED_OUT
- else -> {
- if (hasValidEntitlement) {
- VpnStatus.ACTIVE
- } else {
- VpnStatus.INACTIVE
+ return if (newSettingsFeature.self().isEnabled()) {
+ when {
+ !hasValidEntitlement -> VpnStatus.INELIGIBLE
+ else -> {
+ when (subscriptions.getSubscriptionStatus()) {
+ SubscriptionStatus.INACTIVE, SubscriptionStatus.EXPIRED -> VpnStatus.EXPIRED
+ SubscriptionStatus.UNKNOWN -> VpnStatus.SIGNED_OUT
+ SubscriptionStatus.AUTO_RENEWABLE, SubscriptionStatus.NOT_AUTO_RENEWABLE, SubscriptionStatus.GRACE_PERIOD -> VpnStatus.ACTIVE
+ SubscriptionStatus.WAITING -> VpnStatus.WAITING
+ }
+ }
+ }
+ } else {
+ val subscriptionState = subscriptions.getSubscriptionStatus()
+ when (subscriptionState) {
+ SubscriptionStatus.INACTIVE, SubscriptionStatus.EXPIRED -> VpnStatus.EXPIRED
+ SubscriptionStatus.UNKNOWN -> VpnStatus.SIGNED_OUT
+ else -> {
+ if (hasValidEntitlement) {
+ VpnStatus.ACTIVE
+ } else {
+ VpnStatus.INACTIVE
+ }
}
}
}
diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/LegacyProSettingNetPView.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/LegacyProSettingNetPView.kt
new file mode 100644
index 000000000000..99b2edc9d248
--- /dev/null
+++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/LegacyProSettingNetPView.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2024 DuckDuckGo
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.duckduckgo.networkprotection.impl.subscription.settings
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.FrameLayout
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
+import com.duckduckgo.anvil.annotations.InjectWith
+import com.duckduckgo.common.ui.view.gone
+import com.duckduckgo.common.ui.view.show
+import com.duckduckgo.common.ui.viewbinding.viewBinding
+import com.duckduckgo.di.scopes.ViewScope
+import com.duckduckgo.mobile.android.R as CommonR
+import com.duckduckgo.navigation.api.GlobalActivityStarter
+import com.duckduckgo.networkprotection.impl.databinding.LegacyViewSettingsNetpBinding
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.Command
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.Command.OpenNetPScreen
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.Factory
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState.Hidden
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState.Pending
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState.ShowState
+import dagger.android.support.AndroidSupportInjection
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+
+@InjectWith(ViewScope::class)
+class LegacyProSettingNetPView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0,
+) : FrameLayout(context, attrs, defStyle) {
+
+ @Inject
+ lateinit var viewModelFactory: Factory
+
+ @Inject
+ lateinit var globalActivityStarter: GlobalActivityStarter
+
+ private var coroutineScope: CoroutineScope? = null
+
+ private val binding: LegacyViewSettingsNetpBinding by viewBinding()
+
+ private val viewModel: LegacyProSettingNetPViewModel by lazy {
+ ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[LegacyProSettingNetPViewModel::class.java]
+ }
+
+ override fun onAttachedToWindow() {
+ AndroidSupportInjection.inject(this)
+ super.onAttachedToWindow()
+
+ findViewTreeLifecycleOwner()?.lifecycle?.addObserver(viewModel)
+
+ binding.netpPSetting.setClickListener {
+ viewModel.onNetPSettingClicked()
+ }
+
+ @SuppressLint("NoHardcodedCoroutineDispatcher")
+ coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
+
+ viewModel.viewState
+ .onEach { updateNetPSettings(it.networkProtectionEntryState) }
+ .launchIn(coroutineScope!!)
+
+ viewModel.commands()
+ .onEach { processCommands(it) }
+ .launchIn(coroutineScope!!)
+ }
+
+ private fun updateNetPSettings(networkProtectionEntryState: NetPEntryState) {
+ with(binding.netpPSetting) {
+ when (networkProtectionEntryState) {
+ Hidden -> this.gone()
+ Pending -> {
+ this.show()
+ this.setLeadingIconResource(CommonR.drawable.ic_check_grey_round_16)
+ }
+ is ShowState -> {
+ this.show()
+ this.setLeadingIconResource(networkProtectionEntryState.icon)
+ }
+ }
+ }
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ findViewTreeLifecycleOwner()?.lifecycle?.removeObserver(viewModel)
+ coroutineScope?.cancel()
+ coroutineScope = null
+ }
+
+ private fun processCommands(command: Command) {
+ when (command) {
+ is OpenNetPScreen -> {
+ globalActivityStarter.start(context, command.params)
+ }
+ }
+ }
+}
diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/LegacyProSettingNetPViewModel.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/LegacyProSettingNetPViewModel.kt
new file mode 100644
index 000000000000..686f54ddc531
--- /dev/null
+++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/LegacyProSettingNetPViewModel.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2024 DuckDuckGo
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.duckduckgo.networkprotection.impl.subscription.settings
+
+import android.annotation.SuppressLint
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.duckduckgo.app.statistics.pixels.Pixel
+import com.duckduckgo.common.utils.DispatcherProvider
+import com.duckduckgo.mobile.android.R as CommonR
+import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
+import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState
+import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState.NetPAccessState
+import com.duckduckgo.networkprotection.api.NetworkProtectionState
+import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState
+import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.CONNECTED
+import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.CONNECTING
+import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.DISCONNECTED
+import com.duckduckgo.networkprotection.impl.R
+import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_SETTINGS_PRESSED
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.Command.OpenNetPScreen
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState.Hidden
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState.Pending
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState.ShowState
+import javax.inject.Inject
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import logcat.logcat
+
+@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle
+class LegacyProSettingNetPViewModel(
+ private val networkProtectionAccessState: NetworkProtectionAccessState,
+ private val networkProtectionState: NetworkProtectionState,
+ private val dispatcherProvider: DispatcherProvider,
+ private val pixel: Pixel,
+) : ViewModel(), DefaultLifecycleObserver {
+
+ data class ViewState(val networkProtectionEntryState: NetPEntryState = Hidden)
+
+ sealed class Command {
+ data class OpenNetPScreen(val params: ActivityParams) : Command()
+ }
+
+ sealed class NetPEntryState {
+ data object Hidden : NetPEntryState()
+ data object Pending : NetPEntryState()
+ data class ShowState(
+ @DrawableRes val icon: Int,
+ @StringRes val subtitle: Int,
+ ) : NetPEntryState()
+ }
+
+ private val command = Channel(1, BufferOverflow.DROP_OLDEST)
+ internal fun commands(): Flow = command.receiveAsFlow()
+ private val _viewState = MutableStateFlow(ViewState())
+ val viewState = _viewState.asStateFlow()
+
+ override fun onStart(owner: LifecycleOwner) {
+ super.onStart(owner)
+
+ viewModelScope.launch {
+ combine(networkProtectionAccessState.getStateFlow(), networkProtectionState.getConnectionStateFlow()) { accessState, connectionState ->
+ _viewState.emit(
+ viewState.value.copy(
+ networkProtectionEntryState = getNetworkProtectionEntryState(accessState, connectionState),
+ ),
+ )
+ }.flowOn(dispatcherProvider.main()).launchIn(viewModelScope)
+ }
+ }
+
+ fun onNetPSettingClicked() {
+ viewModelScope.launch {
+ val screen = networkProtectionAccessState.getScreenForCurrentState()
+ screen?.let {
+ command.send(OpenNetPScreen(screen))
+ pixel.fire(NETP_SETTINGS_PRESSED)
+ } ?: logcat { "Get screen for current NetP state is null" }
+ }
+ }
+
+ private suspend fun getNetworkProtectionEntryState(
+ accessState: NetPAccessState,
+ networkProtectionConnectionState: ConnectionState,
+ ): NetPEntryState {
+ return when (accessState) {
+ is NetPAccessState.UnLocked -> {
+ if (networkProtectionState.isOnboarded()) {
+ val subtitle = when (networkProtectionConnectionState) {
+ CONNECTED -> R.string.netpSubscriptionSettingsConnected
+ CONNECTING -> R.string.netpSubscriptionSettingsConnecting
+ else -> R.string.netpSubscriptionSettingsDisconnected
+ }
+
+ val netPItemIcon = if (networkProtectionConnectionState != DISCONNECTED) {
+ CommonR.drawable.ic_check_green_round_16
+ } else {
+ CommonR.drawable.ic_exclamation_yellow_16
+ }
+
+ ShowState(
+ icon = netPItemIcon,
+ subtitle = subtitle,
+ )
+ } else {
+ Pending
+ }
+ }
+
+ NetPAccessState.Locked -> Hidden
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ class Factory @Inject constructor(
+ private val networkProtectionAccessState: NetworkProtectionAccessState,
+ private val networkProtectionState: NetworkProtectionState,
+ private val dispatcherProvider: DispatcherProvider,
+ private val pixel: Pixel,
+ ) : ViewModelProvider.NewInstanceFactory() {
+ override fun create(modelClass: Class): T {
+ return with(modelClass) {
+ when {
+ isAssignableFrom(LegacyProSettingNetPViewModel::class.java) -> LegacyProSettingNetPViewModel(
+ networkProtectionAccessState,
+ networkProtectionState,
+ dispatcherProvider,
+ pixel,
+ )
+
+ else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
+ }
+ } as T
+ }
+ }
+}
diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsState.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsState.kt
new file mode 100644
index 000000000000..9d4df6906dec
--- /dev/null
+++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsState.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2024 DuckDuckGo
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.duckduckgo.networkprotection.impl.subscription.settings
+
+import kotlinx.coroutines.flow.Flow
+
+interface NetworkProtectionSettingsState {
+
+ /**
+ * Returns a flow of the visibility states of NetP
+ * The caller DOES NOT need to specify the dispatcher when calling this method
+ */
+ suspend fun getNetPSettingsStateFlow(): Flow
+
+ /**
+ * If the Netp Settings Item should be visible to the user and it's current subscription state
+ */
+ sealed interface NetPSettingsState {
+
+ sealed interface Visible : NetPSettingsState {
+ data object Subscribed : Visible
+ data object Expired : Visible
+ data object Activating : Visible
+ }
+ data object Hidden : NetPSettingsState
+ }
+}
diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsStateImpl.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsStateImpl.kt
new file mode 100644
index 000000000000..a129e1208617
--- /dev/null
+++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsStateImpl.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2024 DuckDuckGo
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.duckduckgo.networkprotection.impl.subscription.settings
+
+import com.duckduckgo.common.utils.DispatcherProvider
+import com.duckduckgo.di.scopes.AppScope
+import com.duckduckgo.networkprotection.api.NetworkProtectionState
+import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager
+import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus
+import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.ACTIVE
+import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.EXPIRED
+import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.INACTIVE
+import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.INELIGIBLE
+import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.SIGNED_OUT
+import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.WAITING
+import com.duckduckgo.networkprotection.impl.subscription.isActive
+import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState
+import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Hidden
+import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Visible.Activating
+import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Visible.Expired
+import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Visible.Subscribed
+import com.squareup.anvil.annotations.ContributesBinding
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+@ContributesBinding(AppScope::class)
+class NetworkProtectionSettingsStateImpl @Inject constructor(
+ private val dispatcherProvider: DispatcherProvider,
+ private val networkProtectionState: NetworkProtectionState,
+ private val netpSubscriptionManager: NetpSubscriptionManager,
+) : NetworkProtectionSettingsState {
+
+ override suspend fun getNetPSettingsStateFlow(): Flow =
+ netpSubscriptionManager.vpnStatus().map { status ->
+ if (!status.isActive()) {
+ // if entitlement check succeeded and not an active subscription then reset state
+ handleRevokedVPNState()
+ }
+
+ mapToSettingsState(status)
+ }.flowOn(dispatcherProvider.io())
+
+ private fun mapToSettingsState(vpnStatus: VpnStatus): NetPSettingsState = when (vpnStatus) {
+ ACTIVE -> Subscribed
+ INACTIVE, EXPIRED -> Expired
+ WAITING -> Activating
+ SIGNED_OUT, INELIGIBLE -> Hidden
+ }
+
+ private suspend fun handleRevokedVPNState() {
+ if (networkProtectionState.isEnabled()) {
+ networkProtectionState.stop()
+ }
+ }
+}
diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPView.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPView.kt
index c82423994b22..dcfc63c9974c 100644
--- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPView.kt
+++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPView.kt
@@ -20,24 +20,25 @@ import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import com.duckduckgo.anvil.annotations.InjectWith
-import com.duckduckgo.common.ui.view.gone
-import com.duckduckgo.common.ui.view.show
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.di.scopes.ViewScope
-import com.duckduckgo.mobile.android.R as CommonR
import com.duckduckgo.navigation.api.GlobalActivityStarter
+import com.duckduckgo.networkprotection.impl.R
import com.duckduckgo.networkprotection.impl.databinding.ViewSettingsNetpBinding
import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Command
import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Command.OpenNetPScreen
import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Factory
import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState
+import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Activating
+import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Expired
import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Hidden
-import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Pending
-import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.ShowState
+import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Subscribed
import dagger.android.support.AndroidSupportInjection
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -74,10 +75,6 @@ class ProSettingNetPView @JvmOverloads constructor(
findViewTreeLifecycleOwner()?.lifecycle?.addObserver(viewModel)
- binding.netpPSetting.setClickListener {
- viewModel.onNetPSettingClicked()
- }
-
@SuppressLint("NoHardcodedCoroutineDispatcher")
coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
@@ -93,14 +90,21 @@ class ProSettingNetPView @JvmOverloads constructor(
private fun updateNetPSettings(networkProtectionEntryState: NetPEntryState) {
with(binding.netpPSetting) {
when (networkProtectionEntryState) {
- Hidden -> this.gone()
- Pending -> {
- this.show()
- this.setLeadingIconResource(CommonR.drawable.ic_check_grey_round_16)
+ Hidden -> isGone = true
+ Activating,
+ Expired,
+ -> {
+ isVisible = true
+ isClickable = false
+ setLeadingIconResource(R.drawable.ic_vpn_grayscale_color_24)
+ setStatus(isOn = false)
}
- is ShowState -> {
- this.show()
- this.setLeadingIconResource(networkProtectionEntryState.icon)
+ is Subscribed -> {
+ isVisible = true
+ isClickable = true
+ setClickListener { viewModel.onNetPSettingClicked() }
+ setLeadingIconResource(R.drawable.ic_vpn_color_24)
+ setStatus(isOn = networkProtectionEntryState.isActive)
}
}
}
diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModel.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModel.kt
index 866d8ca0887f..303b10d7af77 100644
--- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModel.kt
+++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModel.kt
@@ -17,8 +17,6 @@
package com.duckduckgo.networkprotection.impl.subscription.settings
import android.annotation.SuppressLint
-import androidx.annotation.DrawableRes
-import androidx.annotation.StringRes
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
@@ -26,21 +24,17 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.common.utils.DispatcherProvider
-import com.duckduckgo.mobile.android.R as CommonR
import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState
-import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState.NetPAccessState
import com.duckduckgo.networkprotection.api.NetworkProtectionState
import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState
-import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.CONNECTED
-import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.CONNECTING
-import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.DISCONNECTED
-import com.duckduckgo.networkprotection.impl.R
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_SETTINGS_PRESSED
+import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState
+import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Hidden
+import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Visible.Activating
+import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Visible.Expired
+import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState.Visible.Subscribed
import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Command.OpenNetPScreen
-import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Hidden
-import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Pending
-import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.ShowState
import javax.inject.Inject
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
@@ -56,13 +50,14 @@ import logcat.logcat
@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle
class ProSettingNetPViewModel(
- private val networkProtectionAccessState: NetworkProtectionAccessState,
+ private val networkProtectionSettingsState: NetworkProtectionSettingsState,
private val networkProtectionState: NetworkProtectionState,
+ private val networkProtectionAccessState: NetworkProtectionAccessState,
private val dispatcherProvider: DispatcherProvider,
private val pixel: Pixel,
) : ViewModel(), DefaultLifecycleObserver {
- data class ViewState(val networkProtectionEntryState: NetPEntryState = Hidden)
+ data class ViewState(val networkProtectionEntryState: NetPEntryState = NetPEntryState.Hidden)
sealed class Command {
data class OpenNetPScreen(val params: ActivityParams) : Command()
@@ -70,11 +65,9 @@ class ProSettingNetPViewModel(
sealed class NetPEntryState {
data object Hidden : NetPEntryState()
- data object Pending : NetPEntryState()
- data class ShowState(
- @DrawableRes val icon: Int,
- @StringRes val subtitle: Int,
- ) : NetPEntryState()
+ data class Subscribed(val isActive: Boolean) : NetPEntryState()
+ data object Expired : NetPEntryState()
+ data object Activating : NetPEntryState()
}
private val command = Channel(1, BufferOverflow.DROP_OLDEST)
@@ -86,7 +79,10 @@ class ProSettingNetPViewModel(
super.onStart(owner)
viewModelScope.launch {
- combine(networkProtectionAccessState.getStateFlow(), networkProtectionState.getConnectionStateFlow()) { accessState, connectionState ->
+ combine(
+ networkProtectionSettingsState.getNetPSettingsStateFlow(),
+ networkProtectionState.getConnectionStateFlow(),
+ ) { accessState, connectionState ->
_viewState.emit(
viewState.value.copy(
networkProtectionEntryState = getNetworkProtectionEntryState(accessState, connectionState),
@@ -106,42 +102,22 @@ class ProSettingNetPViewModel(
}
}
- private suspend fun getNetworkProtectionEntryState(
- accessState: NetPAccessState,
+ private fun getNetworkProtectionEntryState(
+ settingsState: NetPSettingsState,
networkProtectionConnectionState: ConnectionState,
- ): NetPEntryState {
- return when (accessState) {
- is NetPAccessState.UnLocked -> {
- if (networkProtectionState.isOnboarded()) {
- val subtitle = when (networkProtectionConnectionState) {
- CONNECTED -> R.string.netpSubscriptionSettingsConnected
- CONNECTING -> R.string.netpSubscriptionSettingsConnecting
- else -> R.string.netpSubscriptionSettingsDisconnected
- }
-
- val netPItemIcon = if (networkProtectionConnectionState != DISCONNECTED) {
- CommonR.drawable.ic_check_green_round_16
- } else {
- CommonR.drawable.ic_exclamation_yellow_16
- }
-
- ShowState(
- icon = netPItemIcon,
- subtitle = subtitle,
- )
- } else {
- Pending
- }
- }
-
- NetPAccessState.Locked -> Hidden
+ ): NetPEntryState =
+ when (settingsState) {
+ Hidden -> NetPEntryState.Hidden
+ Subscribed -> NetPEntryState.Subscribed(isActive = networkProtectionConnectionState.isConnected())
+ Activating -> NetPEntryState.Activating
+ Expired -> NetPEntryState.Expired
}
- }
@Suppress("UNCHECKED_CAST")
class Factory @Inject constructor(
- private val networkProtectionAccessState: NetworkProtectionAccessState,
+ private val networkProtectionSettingsState: NetworkProtectionSettingsState,
private val networkProtectionState: NetworkProtectionState,
+ private val networkProtectionAccessState: NetworkProtectionAccessState,
private val dispatcherProvider: DispatcherProvider,
private val pixel: Pixel,
) : ViewModelProvider.NewInstanceFactory() {
@@ -149,8 +125,9 @@ class ProSettingNetPViewModel(
return with(modelClass) {
when {
isAssignableFrom(ProSettingNetPViewModel::class.java) -> ProSettingNetPViewModel(
- networkProtectionAccessState,
+ networkProtectionSettingsState,
networkProtectionState,
+ networkProtectionAccessState,
dispatcherProvider,
pixel,
)
diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/SubsSettingsPlugin.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/SubsSettingsPlugin.kt
index 108c75960b3d..d721a5dea065 100644
--- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/SubsSettingsPlugin.kt
+++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/SubsSettingsPlugin.kt
@@ -20,14 +20,19 @@ import android.content.Context
import android.view.View
import com.duckduckgo.anvil.annotations.PriorityKey
import com.duckduckgo.di.scopes.ActivityScope
+import com.duckduckgo.settings.api.NewSettingsFeature
import com.duckduckgo.settings.api.ProSettingsPlugin
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject
@ContributesMultibinding(ActivityScope::class)
@PriorityKey(200)
-class ProSettingsNetP @Inject constructor() : ProSettingsPlugin {
+class ProSettingsNetP @Inject constructor(private val newSettingsFeature: NewSettingsFeature) : ProSettingsPlugin {
override fun getView(context: Context): View {
- return ProSettingNetPView(context)
+ return if (newSettingsFeature.self().isEnabled()) {
+ ProSettingNetPView(context)
+ } else {
+ return LegacyProSettingNetPView(context)
+ }
}
}
diff --git a/network-protection/network-protection-impl/src/main/res/layout/legacy_view_settings_netp.xml b/network-protection/network-protection-impl/src/main/res/layout/legacy_view_settings_netp.xml
new file mode 100644
index 000000000000..6fbef946751f
--- /dev/null
+++ b/network-protection/network-protection-impl/src/main/res/layout/legacy_view_settings_netp.xml
@@ -0,0 +1,25 @@
+
+
+
\ No newline at end of file
diff --git a/network-protection/network-protection-impl/src/main/res/layout/view_settings_netp.xml b/network-protection/network-protection-impl/src/main/res/layout/view_settings_netp.xml
index 6fbef946751f..509de9f75387 100644
--- a/network-protection/network-protection-impl/src/main/res/layout/view_settings_netp.xml
+++ b/network-protection/network-protection-impl/src/main/res/layout/view_settings_netp.xml
@@ -14,12 +14,11 @@
~ limitations under the License.
-->
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/LegacyProSettingNetPViewModelTest.kt b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/LegacyProSettingNetPViewModelTest.kt
new file mode 100644
index 000000000000..25866ec4dd9a
--- /dev/null
+++ b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/LegacyProSettingNetPViewModelTest.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) 2024 DuckDuckGo
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.duckduckgo.networkprotection.impl.subscription.settings
+
+import app.cash.turbine.test
+import com.duckduckgo.app.statistics.pixels.Pixel
+import com.duckduckgo.common.test.CoroutineTestRule
+import com.duckduckgo.mobile.android.R as CommonR
+import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
+import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState
+import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState.NetPAccessState.Locked
+import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState.NetPAccessState.UnLocked
+import com.duckduckgo.networkprotection.api.NetworkProtectionState
+import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.CONNECTED
+import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.CONNECTING
+import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.DISCONNECTED
+import com.duckduckgo.networkprotection.impl.R
+import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_SETTINGS_PRESSED
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.Command
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState.Hidden
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState.Pending
+import com.duckduckgo.networkprotection.impl.subscription.settings.LegacyProSettingNetPViewModel.NetPEntryState.ShowState
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class LegacyProSettingNetPViewModelTest {
+
+ @get:Rule
+ val coroutineTestRule: CoroutineTestRule = CoroutineTestRule()
+
+ private val pixel: Pixel = mock()
+ private val networkProtectionState: NetworkProtectionState = mock()
+ private val networkProtectionAccessState: NetworkProtectionAccessState = mock()
+ private lateinit var proSettingNetPViewModel: LegacyProSettingNetPViewModel
+
+ @Before
+ fun before() {
+ proSettingNetPViewModel = LegacyProSettingNetPViewModel(
+ networkProtectionAccessState,
+ networkProtectionState,
+ coroutineTestRule.testDispatcherProvider,
+ pixel,
+ )
+ }
+
+ @Test
+ fun whenNetPSettingClickedThenReturnScreenForCurrentState() = runTest {
+ val testScreen = object : ActivityParams {}
+ whenever(networkProtectionAccessState.getScreenForCurrentState()).thenReturn(testScreen)
+
+ proSettingNetPViewModel.commands().test {
+ proSettingNetPViewModel.onNetPSettingClicked()
+
+ assertEquals(Command.OpenNetPScreen(testScreen), awaitItem())
+ verify(pixel).fire(NETP_SETTINGS_PRESSED)
+
+ cancelAndConsumeRemainingEvents()
+ }
+ }
+
+ @Test
+ fun whenNetPIsNotUnlockedThenNetPEntryStateShouldShowHidden() = runTest {
+ whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED))
+ whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(Locked))
+
+ proSettingNetPViewModel.onStart(mock())
+
+ proSettingNetPViewModel.viewState.test {
+ assertEquals(
+ Hidden,
+ expectMostRecentItem().networkProtectionEntryState,
+ )
+ }
+ }
+
+ @Test
+ fun whenNetPStateIsInBetaButNotAcceptedTermsThenNetPEntryStateShouldShowPending() = runTest {
+ whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED))
+ whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(UnLocked))
+ whenever(networkProtectionState.isOnboarded()).thenReturn(false)
+
+ proSettingNetPViewModel.onStart(mock())
+
+ proSettingNetPViewModel.viewState.test {
+ assertEquals(
+ Pending,
+ expectMostRecentItem().networkProtectionEntryState,
+ )
+ }
+ }
+
+ @Test
+ fun whenNetPStateIsInBetaAndOnboardedAndEnabledThenNetPEntryStateShouldCorrectShowState() = runTest {
+ whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(CONNECTED))
+ whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(UnLocked))
+ whenever(networkProtectionState.isOnboarded()).thenReturn(true)
+
+ proSettingNetPViewModel.onStart(mock())
+
+ proSettingNetPViewModel.viewState.test {
+ assertEquals(
+ ShowState(
+ icon = CommonR.drawable.ic_check_green_round_16,
+ subtitle = R.string.netpSubscriptionSettingsConnected,
+ ),
+ expectMostRecentItem().networkProtectionEntryState,
+ )
+ }
+ }
+
+ @Test
+ fun whenNetPStateIsInBetaAndNotOnboardedAndEnabledThenNetPEntryStateShouldCorrectShowState() = runTest {
+ whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(CONNECTED))
+ whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(UnLocked))
+ whenever(networkProtectionState.isOnboarded()).thenReturn(false)
+
+ proSettingNetPViewModel.onStart(mock())
+
+ proSettingNetPViewModel.viewState.test {
+ assertEquals(
+ Pending,
+ expectMostRecentItem().networkProtectionEntryState,
+ )
+ }
+ }
+
+ @Test
+ fun whenNetPStateIsInBetaOnboardedAndEnabledThenNetPEntryStateShouldCorrectShowState() = runTest {
+ whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(CONNECTED))
+ whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(UnLocked))
+ whenever(networkProtectionState.isOnboarded()).thenReturn(true)
+
+ proSettingNetPViewModel.onStart(mock())
+
+ proSettingNetPViewModel.viewState.test {
+ assertEquals(
+ ShowState(
+ icon = CommonR.drawable.ic_check_green_round_16,
+ subtitle = R.string.netpSubscriptionSettingsConnected,
+ ),
+ expectMostRecentItem().networkProtectionEntryState,
+ )
+ }
+ }
+
+ @Test
+ fun whenNetPStateIsInBetaAndConnectingThenNetPEntryStateShouldCorrectShowState() = runTest {
+ whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(CONNECTING))
+ whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(UnLocked))
+ whenever(networkProtectionState.isOnboarded()).thenReturn(true)
+
+ proSettingNetPViewModel.onStart(mock())
+
+ proSettingNetPViewModel.viewState.test {
+ assertEquals(
+ ShowState(
+ icon = CommonR.drawable.ic_check_green_round_16,
+ subtitle = R.string.netpSubscriptionSettingsConnecting,
+ ),
+ expectMostRecentItem().networkProtectionEntryState,
+ )
+ }
+ }
+
+ @Test
+ fun whenNetPStateIsInBetaAndDisabledThenNetPEntryStateShouldCorrectShowState() = runTest {
+ whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED))
+ whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(UnLocked))
+ whenever(networkProtectionState.isOnboarded()).thenReturn(true)
+
+ proSettingNetPViewModel.onStart(mock())
+
+ proSettingNetPViewModel.viewState.test {
+ assertEquals(
+ ShowState(
+ icon = CommonR.drawable.ic_exclamation_yellow_16,
+ subtitle = R.string.netpSubscriptionSettingsDisconnected,
+ ),
+ expectMostRecentItem().networkProtectionEntryState,
+ )
+ }
+ }
+}
diff --git a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsStateImplTest.kt b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsStateImplTest.kt
new file mode 100644
index 000000000000..c95f916d502d
--- /dev/null
+++ b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/NetworkProtectionSettingsStateImplTest.kt
@@ -0,0 +1,124 @@
+package com.duckduckgo.networkprotection.impl.subscription.settings
+
+import app.cash.turbine.test
+import com.duckduckgo.common.test.CoroutineTestRule
+import com.duckduckgo.networkprotection.api.NetworkProtectionState
+import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager
+import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus
+import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class NetworkProtectionSettingsStateImplTest {
+
+ @get:Rule
+ val coroutineTestRule = CoroutineTestRule()
+
+ private lateinit var networkProtectionSettingsState: NetworkProtectionSettingsStateImpl
+ private lateinit var fakeNetworkProtectionState: FakeNetworkProtectionState
+ private lateinit var fakeNetpSubscriptionManager: FakeNetpSubscriptionManager
+
+ @Before
+ fun setUp() {
+ fakeNetworkProtectionState = FakeNetworkProtectionState()
+ fakeNetpSubscriptionManager = FakeNetpSubscriptionManager()
+
+ networkProtectionSettingsState = NetworkProtectionSettingsStateImpl(
+ dispatcherProvider = coroutineTestRule.testDispatcherProvider,
+ networkProtectionState = fakeNetworkProtectionState,
+ netpSubscriptionManager = fakeNetpSubscriptionManager,
+ )
+ }
+
+ @Test
+ fun `when VpnStatus is active then returns subscribed`() = runTest {
+ fakeNetpSubscriptionManager.setVpnStatus(VpnStatus.ACTIVE)
+
+ networkProtectionSettingsState.getNetPSettingsStateFlow().test {
+ assertEquals(NetPSettingsState.Visible.Subscribed, awaitItem())
+ awaitComplete()
+ }
+ }
+
+ @Test
+ fun `when VpnStatus is inactive then returns expired`() = runTest {
+ fakeNetpSubscriptionManager.setVpnStatus(VpnStatus.INACTIVE)
+
+ networkProtectionSettingsState.getNetPSettingsStateFlow().test {
+ assertEquals(NetPSettingsState.Visible.Expired, awaitItem())
+ awaitComplete()
+ }
+ }
+
+ @Test
+ fun `when VpnStatus is expired then returns expired`() = runTest {
+ fakeNetpSubscriptionManager.setVpnStatus(VpnStatus.EXPIRED)
+
+ networkProtectionSettingsState.getNetPSettingsStateFlow().test {
+ assertEquals(NetPSettingsState.Visible.Expired, awaitItem())
+ awaitComplete()
+ }
+ }
+
+ @Test
+ fun `when VpnStatus is activating then returns waiting`() = runTest {
+ fakeNetpSubscriptionManager.setVpnStatus(VpnStatus.WAITING)
+
+ networkProtectionSettingsState.getNetPSettingsStateFlow().test {
+ assertEquals(NetPSettingsState.Visible.Activating, awaitItem())
+ awaitComplete()
+ }
+ }
+
+ @Test
+ fun `when VpnStatus is signed out then returns hidden`() = runTest {
+ fakeNetpSubscriptionManager.setVpnStatus(VpnStatus.SIGNED_OUT)
+
+ networkProtectionSettingsState.getNetPSettingsStateFlow().test {
+ assertEquals(NetPSettingsState.Hidden, awaitItem())
+ awaitComplete()
+ }
+ }
+
+ @Test
+ fun `when VpnStatus is ineligible then returns hidden`() = runTest {
+ fakeNetpSubscriptionManager.setVpnStatus(VpnStatus.INELIGIBLE)
+
+ networkProtectionSettingsState.getNetPSettingsStateFlow().test {
+ assertEquals(NetPSettingsState.Hidden, awaitItem())
+ awaitComplete()
+ }
+ }
+}
+
+private class FakeNetworkProtectionState : NetworkProtectionState {
+ override suspend fun isOnboarded(): Boolean = false
+ override suspend fun isEnabled(): Boolean = false
+ override suspend fun isRunning(): Boolean = false
+ override fun start() {}
+ override fun restart() {}
+ override fun clearVPNConfigurationAndRestart() {}
+ override suspend fun stop() {}
+ override fun clearVPNConfigurationAndStop() {}
+ override fun serverLocation(): String? = null
+ override fun getConnectionStateFlow(): Flow = flowOf()
+ override suspend fun getExcludedApps(): List = emptyList()
+}
+
+private class FakeNetpSubscriptionManager : NetpSubscriptionManager {
+
+ private var vpnStatusFlow: Flow = flowOf()
+
+ override suspend fun getVpnStatus(): VpnStatus = vpnStatusFlow.first()
+ override suspend fun vpnStatus(): Flow = vpnStatusFlow
+
+ fun setVpnStatus(status: VpnStatus) {
+ vpnStatusFlow = flowOf(status)
+ }
+}
diff --git a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModelTest.kt b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModelTest.kt
index f46553aad3a8..68d72548e687 100644
--- a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModelTest.kt
+++ b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModelTest.kt
@@ -19,21 +19,19 @@ package com.duckduckgo.networkprotection.impl.subscription.settings
import app.cash.turbine.test
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.common.test.CoroutineTestRule
-import com.duckduckgo.mobile.android.R as CommonR
import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState
-import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState.NetPAccessState.Locked
-import com.duckduckgo.networkprotection.api.NetworkProtectionAccessState.NetPAccessState.UnLocked
import com.duckduckgo.networkprotection.api.NetworkProtectionState
import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.CONNECTED
import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.CONNECTING
import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.DISCONNECTED
-import com.duckduckgo.networkprotection.impl.R
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_SETTINGS_PRESSED
+import com.duckduckgo.networkprotection.impl.subscription.settings.NetworkProtectionSettingsState.NetPSettingsState
import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Command
+import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Activating
+import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Expired
import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Hidden
-import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Pending
-import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.ShowState
+import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Subscribed
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
@@ -52,20 +50,22 @@ class ProSettingNetPViewModelTest {
private val pixel: Pixel = mock()
private val networkProtectionState: NetworkProtectionState = mock()
private val networkProtectionAccessState: NetworkProtectionAccessState = mock()
+ private val networkProtectionSettingsState: NetworkProtectionSettingsState = mock()
private lateinit var proSettingNetPViewModel: ProSettingNetPViewModel
@Before
fun before() {
proSettingNetPViewModel = ProSettingNetPViewModel(
- networkProtectionAccessState,
+ networkProtectionSettingsState,
networkProtectionState,
+ networkProtectionAccessState,
coroutineTestRule.testDispatcherProvider,
pixel,
)
}
@Test
- fun whenNetPSettingClickedThenReturnScreenForCurrentState() = runTest {
+ fun whenNetPSettingClickedThenNetPScreenOpened() = runTest {
val testScreen = object : ActivityParams {}
whenever(networkProtectionAccessState.getScreenForCurrentState()).thenReturn(testScreen)
@@ -80,9 +80,9 @@ class ProSettingNetPViewModelTest {
}
@Test
- fun whenNetPIsNotUnlockedThenNetPEntryStateShouldShowHidden() = runTest {
+ fun whenNetPVisibilityStateIsHiddenThenNetPEntryStateIsHidden() = runTest {
whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED))
- whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(Locked))
+ whenever(networkProtectionSettingsState.getNetPSettingsStateFlow()).thenReturn(flowOf(NetPSettingsState.Hidden))
proSettingNetPViewModel.onStart(mock())
@@ -95,108 +95,75 @@ class ProSettingNetPViewModelTest {
}
@Test
- fun whenNetPStateIsInBetaButNotAcceptedTermsThenNetPEntryStateShouldShowPending() = runTest {
+ fun whenNetPVisibilityStateIsActivatingThenNetPEntryStateIsActivating() = runTest {
whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED))
- whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(UnLocked))
- whenever(networkProtectionState.isOnboarded()).thenReturn(false)
-
- proSettingNetPViewModel.onStart(mock())
-
- proSettingNetPViewModel.viewState.test {
- assertEquals(
- Pending,
- expectMostRecentItem().networkProtectionEntryState,
- )
- }
- }
-
- @Test
- fun whenNetPStateIsInBetaAndOnboardedAndEnabledThenNetPEntryStateShouldCorrectShowState() = runTest {
- whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(CONNECTED))
- whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(UnLocked))
- whenever(networkProtectionState.isOnboarded()).thenReturn(true)
+ whenever(networkProtectionSettingsState.getNetPSettingsStateFlow()).thenReturn(flowOf(NetPSettingsState.Visible.Activating))
proSettingNetPViewModel.onStart(mock())
proSettingNetPViewModel.viewState.test {
assertEquals(
- ShowState(
- icon = CommonR.drawable.ic_check_green_round_16,
- subtitle = R.string.netpSubscriptionSettingsConnected,
- ),
+ Activating,
expectMostRecentItem().networkProtectionEntryState,
)
}
}
@Test
- fun whenNetPStateIsInBetaAndNotOnboardedAndEnabledThenNetPEntryStateShouldCorrectShowState() = runTest {
+ fun whenNetPVisibilityStateConnectedAndAccessStateIsSubscribedThenNetPEntryStateIsSubscribedAndActive() = runTest {
whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(CONNECTED))
- whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(UnLocked))
- whenever(networkProtectionState.isOnboarded()).thenReturn(false)
+ whenever(networkProtectionSettingsState.getNetPSettingsStateFlow()).thenReturn(flowOf(NetPSettingsState.Visible.Subscribed))
proSettingNetPViewModel.onStart(mock())
proSettingNetPViewModel.viewState.test {
assertEquals(
- Pending,
+ Subscribed(isActive = true),
expectMostRecentItem().networkProtectionEntryState,
)
}
}
@Test
- fun whenNetPStateIsInBetaOnboardedAndEnabledThenNetPEntryStateShouldCorrectShowState() = runTest {
- whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(CONNECTED))
- whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(UnLocked))
- whenever(networkProtectionState.isOnboarded()).thenReturn(true)
+ fun whenNetPVisibilityStateDisconnectedAndAccessStateIsSubscribedThenNetPEntryStateIsSubscribedAndInactive() = runTest {
+ whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED))
+ whenever(networkProtectionSettingsState.getNetPSettingsStateFlow()).thenReturn(flowOf(NetPSettingsState.Visible.Subscribed))
proSettingNetPViewModel.onStart(mock())
proSettingNetPViewModel.viewState.test {
assertEquals(
- ShowState(
- icon = CommonR.drawable.ic_check_green_round_16,
- subtitle = R.string.netpSubscriptionSettingsConnected,
- ),
+ Subscribed(isActive = false),
expectMostRecentItem().networkProtectionEntryState,
)
}
}
@Test
- fun whenNetPStateIsInBetaAndConnectingThenNetPEntryStateShouldCorrectShowState() = runTest {
+ fun whenNetPVisibilityStateIsConnectingThenNetPEntryStateIsSubscribedAndNotActive() = runTest {
whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(CONNECTING))
- whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(UnLocked))
- whenever(networkProtectionState.isOnboarded()).thenReturn(true)
+ whenever(networkProtectionSettingsState.getNetPSettingsStateFlow()).thenReturn(flowOf(NetPSettingsState.Visible.Subscribed))
proSettingNetPViewModel.onStart(mock())
proSettingNetPViewModel.viewState.test {
assertEquals(
- ShowState(
- icon = CommonR.drawable.ic_check_green_round_16,
- subtitle = R.string.netpSubscriptionSettingsConnecting,
- ),
+ Subscribed(isActive = false),
expectMostRecentItem().networkProtectionEntryState,
)
}
}
@Test
- fun whenNetPStateIsInBetaAndDisabledThenNetPEntryStateShouldCorrectShowState() = runTest {
+ fun whenNetPVisibilityStateIsExpiredThenNetPEntryStateIsExpired() = runTest {
whenever(networkProtectionState.getConnectionStateFlow()).thenReturn(flowOf(DISCONNECTED))
- whenever(networkProtectionAccessState.getStateFlow()).thenReturn(flowOf(UnLocked))
- whenever(networkProtectionState.isOnboarded()).thenReturn(true)
+ whenever(networkProtectionSettingsState.getNetPSettingsStateFlow()).thenReturn(flowOf(NetPSettingsState.Visible.Expired))
proSettingNetPViewModel.onStart(mock())
proSettingNetPViewModel.viewState.test {
assertEquals(
- ShowState(
- icon = CommonR.drawable.ic_exclamation_yellow_16,
- subtitle = R.string.netpSubscriptionSettingsDisconnected,
- ),
+ Expired,
expectMostRecentItem().networkProtectionEntryState,
)
}
diff --git a/node_modules/@duckduckgo/autofill/dist/autofill-debug.js b/node_modules/@duckduckgo/autofill/dist/autofill-debug.js
index b02ab092e3a5..a3ad7f79835a 100644
--- a/node_modules/@duckduckgo/autofill/dist/autofill-debug.js
+++ b/node_modules/@duckduckgo/autofill/dist/autofill-debug.js
@@ -138,6 +138,11 @@ class ZodError extends Error {
processError(this);
return fieldErrors;
}
+ static assert(value) {
+ if (!(value instanceof ZodError)) {
+ throw new Error(`Not a ZodError: ${value}`);
+ }
+ }
toString() {
return this.message;
}
@@ -267,6 +272,13 @@ const makeIssue = params => {
...issueData,
path: fullPath
};
+ if (issueData.message !== undefined) {
+ return {
+ ...issueData,
+ path: fullPath,
+ message: issueData.message
+ };
+ }
let errorMessage = "";
const maps = errorMaps.filter(m => !!m).slice().reverse();
for (const map of maps) {
@@ -278,17 +290,18 @@ const makeIssue = params => {
return {
...issueData,
path: fullPath,
- message: issueData.message || errorMessage
+ message: errorMessage
};
};
exports.makeIssue = makeIssue;
exports.EMPTY_PATH = [];
function addIssueToContext(ctx, issueData) {
+ const overrideMap = (0, errors_1.getErrorMap)();
const issue = (0, exports.makeIssue)({
issueData: issueData,
data: ctx.data,
path: ctx.path,
- errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, (0, errors_1.getErrorMap)(), en_1.default // then global default map
+ errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, overrideMap, overrideMap === en_1.default ? undefined : en_1.default // then global default map
].filter(x => !!x)
});
ctx.common.issues.push(issue);
@@ -319,9 +332,11 @@ class ParseStatus {
static async mergeObjectAsync(status, pairs) {
const syncPairs = [];
for (const pair of pairs) {
+ const key = await pair.key;
+ const value = await pair.value;
syncPairs.push({
- key: await pair.key,
- value: await pair.value
+ key,
+ value
});
}
return ParseStatus.mergeObjectSync(status, syncPairs);
@@ -632,11 +647,23 @@ exports.default = errorMap;
},{"../ZodError":2,"../helpers/util":8}],11:[function(require,module,exports){
"use strict";
+var __classPrivateFieldGet = void 0 && (void 0).__classPrivateFieldGet || function (receiver, state, kind, f) {
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
+};
+var __classPrivateFieldSet = void 0 && (void 0).__classPrivateFieldSet || function (receiver, state, value, kind, f) {
+ if (kind === "m") throw new TypeError("Private method is not writable");
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
+ return kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value), value;
+};
+var _ZodEnum_cache, _ZodNativeEnum_cache;
Object.defineProperty(exports, "__esModule", {
value: true
});
-exports.date = exports.boolean = exports.bigint = exports.array = exports.any = exports.coerce = exports.ZodFirstPartyTypeKind = exports.late = exports.ZodSchema = exports.Schema = exports.custom = exports.ZodReadonly = exports.ZodPipeline = exports.ZodBranded = exports.BRAND = exports.ZodNaN = exports.ZodCatch = exports.ZodDefault = exports.ZodNullable = exports.ZodOptional = exports.ZodTransformer = exports.ZodEffects = exports.ZodPromise = exports.ZodNativeEnum = exports.ZodEnum = exports.ZodLiteral = exports.ZodLazy = exports.ZodFunction = exports.ZodSet = exports.ZodMap = exports.ZodRecord = exports.ZodTuple = exports.ZodIntersection = exports.ZodDiscriminatedUnion = exports.ZodUnion = exports.ZodObject = exports.ZodArray = exports.ZodVoid = exports.ZodNever = exports.ZodUnknown = exports.ZodAny = exports.ZodNull = exports.ZodUndefined = exports.ZodSymbol = exports.ZodDate = exports.ZodBoolean = exports.ZodBigInt = exports.ZodNumber = exports.ZodString = exports.ZodType = void 0;
-exports.NEVER = exports.void = exports.unknown = exports.union = exports.undefined = exports.tuple = exports.transformer = exports.symbol = exports.string = exports.strictObject = exports.set = exports.record = exports.promise = exports.preprocess = exports.pipeline = exports.ostring = exports.optional = exports.onumber = exports.oboolean = exports.object = exports.number = exports.nullable = exports.null = exports.never = exports.nativeEnum = exports.nan = exports.map = exports.literal = exports.lazy = exports.intersection = exports.instanceof = exports.function = exports.enum = exports.effect = exports.discriminatedUnion = void 0;
+exports.boolean = exports.bigint = exports.array = exports.any = exports.coerce = exports.ZodFirstPartyTypeKind = exports.late = exports.ZodSchema = exports.Schema = exports.custom = exports.ZodReadonly = exports.ZodPipeline = exports.ZodBranded = exports.BRAND = exports.ZodNaN = exports.ZodCatch = exports.ZodDefault = exports.ZodNullable = exports.ZodOptional = exports.ZodTransformer = exports.ZodEffects = exports.ZodPromise = exports.ZodNativeEnum = exports.ZodEnum = exports.ZodLiteral = exports.ZodLazy = exports.ZodFunction = exports.ZodSet = exports.ZodMap = exports.ZodRecord = exports.ZodTuple = exports.ZodIntersection = exports.ZodDiscriminatedUnion = exports.ZodUnion = exports.ZodObject = exports.ZodArray = exports.ZodVoid = exports.ZodNever = exports.ZodUnknown = exports.ZodAny = exports.ZodNull = exports.ZodUndefined = exports.ZodSymbol = exports.ZodDate = exports.ZodBoolean = exports.ZodBigInt = exports.ZodNumber = exports.ZodString = exports.datetimeRegex = exports.ZodType = void 0;
+exports.NEVER = exports.void = exports.unknown = exports.union = exports.undefined = exports.tuple = exports.transformer = exports.symbol = exports.string = exports.strictObject = exports.set = exports.record = exports.promise = exports.preprocess = exports.pipeline = exports.ostring = exports.optional = exports.onumber = exports.oboolean = exports.object = exports.number = exports.nullable = exports.null = exports.never = exports.nativeEnum = exports.nan = exports.map = exports.literal = exports.lazy = exports.intersection = exports.instanceof = exports.function = exports.enum = exports.effect = exports.discriminatedUnion = exports.date = void 0;
const errors_1 = require("./errors");
const errorUtil_1 = require("./helpers/errorUtil");
const parseUtil_1 = require("./helpers/parseUtil");
@@ -698,16 +725,25 @@ function processCreateParams(params) {
description
};
const customMap = (iss, ctx) => {
- if (iss.code !== "invalid_type") return {
- message: ctx.defaultError
- };
+ var _a, _b;
+ const {
+ message
+ } = params;
+ if (iss.code === "invalid_enum_value") {
+ return {
+ message: message !== null && message !== void 0 ? message : ctx.defaultError
+ };
+ }
if (typeof ctx.data === "undefined") {
return {
- message: required_error !== null && required_error !== void 0 ? required_error : ctx.defaultError
+ message: (_a = message !== null && message !== void 0 ? message : required_error) !== null && _a !== void 0 ? _a : ctx.defaultError
};
}
+ if (iss.code !== "invalid_type") return {
+ message: ctx.defaultError
+ };
return {
- message: invalid_type_error !== null && invalid_type_error !== void 0 ? invalid_type_error : ctx.defaultError
+ message: (_b = message !== null && message !== void 0 ? message : invalid_type_error) !== null && _b !== void 0 ? _b : ctx.defaultError
};
};
return {
@@ -977,11 +1013,13 @@ exports.ZodType = ZodType;
exports.Schema = ZodType;
exports.ZodSchema = ZodType;
const cuidRegex = /^c[^\s-]{8,}$/i;
-const cuid2Regex = /^[a-z][a-z0-9]*$/;
+const cuid2Regex = /^[0-9a-z]+$/;
const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/;
// const uuidRegex =
// /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i;
const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i;
+const nanoidRegex = /^[a-z0-9_-]{21}$/i;
+const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/;
// from https://stackoverflow.com/a/46181/1550155
// old version: too slow, didn't support unicode
// const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i;
@@ -994,36 +1032,47 @@ const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-
// /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
// const emailRegex =
// /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i;
-const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_+-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i;
+const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i;
// const emailRegex =
// /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i;
// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression
const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`;
let emojiRegex;
-const ipv4Regex = /^(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))$/;
+// faster, simpler, safer
+const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
const ipv6Regex = /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/;
-// Adapted from https://stackoverflow.com/a/3143231
-const datetimeRegex = args => {
+// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript
+const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
+// simple
+// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`;
+// no leap year validation
+// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`;
+// with leap year validation
+const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`;
+const dateRegex = new RegExp(`^${dateRegexSource}$`);
+function timeRegexSource(args) {
+ // let regex = `\\d{2}:\\d{2}:\\d{2}`;
+ let regex = `([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d`;
if (args.precision) {
- if (args.offset) {
- return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{${args.precision}}(([+-]\\d{2}(:?\\d{2})?)|Z)$`);
- } else {
- return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{${args.precision}}Z$`);
- }
- } else if (args.precision === 0) {
- if (args.offset) {
- return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(([+-]\\d{2}(:?\\d{2})?)|Z)$`);
- } else {
- return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$`);
- }
- } else {
- if (args.offset) {
- return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}(:?\\d{2})?)|Z)$`);
- } else {
- return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`);
- }
+ regex = `${regex}\\.\\d{${args.precision}}`;
+ } else if (args.precision == null) {
+ regex = `${regex}(\\.\\d+)?`;
}
-};
+ return regex;
+}
+function timeRegex(args) {
+ return new RegExp(`^${timeRegexSource(args)}$`);
+}
+// Adapted from https://stackoverflow.com/a/3143231
+function datetimeRegex(args) {
+ let regex = `${dateRegexSource}T${timeRegexSource(args)}`;
+ const opts = [];
+ opts.push(args.local ? `Z?` : `Z`);
+ if (args.offset) opts.push(`([+-]\\d{2}:?\\d{2})`);
+ regex = `${regex}(${opts.join("|")})`;
+ return new RegExp(`^${regex}$`);
+}
+exports.datetimeRegex = datetimeRegex;
function isValidIP(ip, version) {
if ((version === "v4" || !version) && ipv4Regex.test(ip)) {
return true;
@@ -1045,10 +1094,7 @@ class ZodString extends ZodType {
code: ZodError_1.ZodIssueCode.invalid_type,
expected: util_1.ZodParsedType.string,
received: ctx.parsedType
- }
- //
- );
-
+ });
return parseUtil_1.INVALID;
}
const status = new parseUtil_1.ParseStatus();
@@ -1139,6 +1185,16 @@ class ZodString extends ZodType {
});
status.dirty();
}
+ } else if (check.kind === "nanoid") {
+ if (!nanoidRegex.test(input.data)) {
+ ctx = this._getOrReturnCtx(input, ctx);
+ (0, parseUtil_1.addIssueToContext)(ctx, {
+ validation: "nanoid",
+ code: ZodError_1.ZodIssueCode.invalid_string,
+ message: check.message
+ });
+ status.dirty();
+ }
} else if (check.kind === "cuid") {
if (!cuidRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
@@ -1247,6 +1303,38 @@ class ZodString extends ZodType {
});
status.dirty();
}
+ } else if (check.kind === "date") {
+ const regex = dateRegex;
+ if (!regex.test(input.data)) {
+ ctx = this._getOrReturnCtx(input, ctx);
+ (0, parseUtil_1.addIssueToContext)(ctx, {
+ code: ZodError_1.ZodIssueCode.invalid_string,
+ validation: "date",
+ message: check.message
+ });
+ status.dirty();
+ }
+ } else if (check.kind === "time") {
+ const regex = timeRegex(check);
+ if (!regex.test(input.data)) {
+ ctx = this._getOrReturnCtx(input, ctx);
+ (0, parseUtil_1.addIssueToContext)(ctx, {
+ code: ZodError_1.ZodIssueCode.invalid_string,
+ validation: "time",
+ message: check.message
+ });
+ status.dirty();
+ }
+ } else if (check.kind === "duration") {
+ if (!durationRegex.test(input.data)) {
+ ctx = this._getOrReturnCtx(input, ctx);
+ (0, parseUtil_1.addIssueToContext)(ctx, {
+ validation: "duration",
+ code: ZodError_1.ZodIssueCode.invalid_string,
+ message: check.message
+ });
+ status.dirty();
+ }
} else if (check.kind === "ip") {
if (!isValidIP(input.data, check.version)) {
ctx = this._getOrReturnCtx(input, ctx);
@@ -1257,6 +1345,16 @@ class ZodString extends ZodType {
});
status.dirty();
}
+ } else if (check.kind === "base64") {
+ if (!base64Regex.test(input.data)) {
+ ctx = this._getOrReturnCtx(input, ctx);
+ (0, parseUtil_1.addIssueToContext)(ctx, {
+ validation: "base64",
+ code: ZodError_1.ZodIssueCode.invalid_string,
+ message: check.message
+ });
+ status.dirty();
+ }
} else {
util_1.util.assertNever(check);
}
@@ -1303,6 +1401,12 @@ class ZodString extends ZodType {
...errorUtil_1.errorUtil.errToObj(message)
});
}
+ nanoid(message) {
+ return this._addCheck({
+ kind: "nanoid",
+ ...errorUtil_1.errorUtil.errToObj(message)
+ });
+ }
cuid(message) {
return this._addCheck({
kind: "cuid",
@@ -1321,6 +1425,12 @@ class ZodString extends ZodType {
...errorUtil_1.errorUtil.errToObj(message)
});
}
+ base64(message) {
+ return this._addCheck({
+ kind: "base64",
+ ...errorUtil_1.errorUtil.errToObj(message)
+ });
+ }
ip(options) {
return this._addCheck({
kind: "ip",
@@ -1328,12 +1438,13 @@ class ZodString extends ZodType {
});
}
datetime(options) {
- var _a;
+ var _a, _b;
if (typeof options === "string") {
return this._addCheck({
kind: "datetime",
precision: null,
offset: false,
+ local: false,
message: options
});
}
@@ -1341,9 +1452,36 @@ class ZodString extends ZodType {
kind: "datetime",
precision: typeof (options === null || options === void 0 ? void 0 : options.precision) === "undefined" ? null : options === null || options === void 0 ? void 0 : options.precision,
offset: (_a = options === null || options === void 0 ? void 0 : options.offset) !== null && _a !== void 0 ? _a : false,
+ local: (_b = options === null || options === void 0 ? void 0 : options.local) !== null && _b !== void 0 ? _b : false,
+ ...errorUtil_1.errorUtil.errToObj(options === null || options === void 0 ? void 0 : options.message)
+ });
+ }
+ date(message) {
+ return this._addCheck({
+ kind: "date",
+ message
+ });
+ }
+ time(options) {
+ if (typeof options === "string") {
+ return this._addCheck({
+ kind: "time",
+ precision: null,
+ message: options
+ });
+ }
+ return this._addCheck({
+ kind: "time",
+ precision: typeof (options === null || options === void 0 ? void 0 : options.precision) === "undefined" ? null : options === null || options === void 0 ? void 0 : options.precision,
...errorUtil_1.errorUtil.errToObj(options === null || options === void 0 ? void 0 : options.message)
});
}
+ duration(message) {
+ return this._addCheck({
+ kind: "duration",
+ ...errorUtil_1.errorUtil.errToObj(message)
+ });
+ }
regex(regex, message) {
return this._addCheck({
kind: "regex",
@@ -1428,6 +1566,15 @@ class ZodString extends ZodType {
get isDatetime() {
return !!this._def.checks.find(ch => ch.kind === "datetime");
}
+ get isDate() {
+ return !!this._def.checks.find(ch => ch.kind === "date");
+ }
+ get isTime() {
+ return !!this._def.checks.find(ch => ch.kind === "time");
+ }
+ get isDuration() {
+ return !!this._def.checks.find(ch => ch.kind === "duration");
+ }
get isEmail() {
return !!this._def.checks.find(ch => ch.kind === "email");
}
@@ -1440,6 +1587,9 @@ class ZodString extends ZodType {
get isUUID() {
return !!this._def.checks.find(ch => ch.kind === "uuid");
}
+ get isNANOID() {
+ return !!this._def.checks.find(ch => ch.kind === "nanoid");
+ }
get isCUID() {
return !!this._def.checks.find(ch => ch.kind === "cuid");
}
@@ -1452,6 +1602,9 @@ class ZodString extends ZodType {
get isIP() {
return !!this._def.checks.find(ch => ch.kind === "ip");
}
+ get isBase64() {
+ return !!this._def.checks.find(ch => ch.kind === "base64");
+ }
get minLength() {
let min = null;
for (const ch of this._def.checks) {
@@ -2442,9 +2595,10 @@ class ZodObject extends ZodType {
const syncPairs = [];
for (const pair of pairs) {
const key = await pair.key;
+ const value = await pair.value;
syncPairs.push({
key,
- value: await pair.value,
+ value,
alwaysSet: pair.alwaysSet
});
}
@@ -2814,15 +2968,25 @@ const getDiscriminator = type => {
return type.options;
} else if (type instanceof ZodNativeEnum) {
// eslint-disable-next-line ban/ban
- return Object.keys(type.enum);
+ return util_1.util.objectValues(type.enum);
} else if (type instanceof ZodDefault) {
return getDiscriminator(type._def.innerType);
} else if (type instanceof ZodUndefined) {
return [undefined];
} else if (type instanceof ZodNull) {
return [null];
+ } else if (type instanceof ZodOptional) {
+ return [undefined, ...getDiscriminator(type.unwrap())];
+ } else if (type instanceof ZodNullable) {
+ return [null, ...getDiscriminator(type.unwrap())];
+ } else if (type instanceof ZodBranded) {
+ return getDiscriminator(type.unwrap());
+ } else if (type instanceof ZodReadonly) {
+ return getDiscriminator(type.unwrap());
+ } else if (type instanceof ZodCatch) {
+ return getDiscriminator(type._def.innerType);
} else {
- return null;
+ return [];
}
};
class ZodDiscriminatedUnion extends ZodType {
@@ -2886,7 +3050,7 @@ class ZodDiscriminatedUnion extends ZodType {
// try {
for (const type of options) {
const discriminatorValues = getDiscriminator(type.shape[discriminator]);
- if (!discriminatorValues) {
+ if (!discriminatorValues.length) {
throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`);
}
for (const value of discriminatorValues) {
@@ -3123,7 +3287,8 @@ class ZodRecord extends ZodType {
for (const key in ctx.data) {
pairs.push({
key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)),
- value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key))
+ value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)),
+ alwaysSet: key in ctx.data
});
}
if (ctx.common.async) {
@@ -3511,6 +3676,10 @@ function createZodEnum(values, params) {
});
}
class ZodEnum extends ZodType {
+ constructor() {
+ super(...arguments);
+ _ZodEnum_cache.set(this, void 0);
+ }
_parse(input) {
if (typeof input.data !== "string") {
const ctx = this._getOrReturnCtx(input);
@@ -3522,7 +3691,10 @@ class ZodEnum extends ZodType {
});
return parseUtil_1.INVALID;
}
- if (this._def.values.indexOf(input.data) === -1) {
+ if (!__classPrivateFieldGet(this, _ZodEnum_cache, "f")) {
+ __classPrivateFieldSet(this, _ZodEnum_cache, new Set(this._def.values), "f");
+ }
+ if (!__classPrivateFieldGet(this, _ZodEnum_cache, "f").has(input.data)) {
const ctx = this._getOrReturnCtx(input);
const expectedValues = this._def.values;
(0, parseUtil_1.addIssueToContext)(ctx, {
@@ -3559,15 +3731,28 @@ class ZodEnum extends ZodType {
return enumValues;
}
extract(values) {
- return ZodEnum.create(values);
+ let newDef = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this._def;
+ return ZodEnum.create(values, {
+ ...this._def,
+ ...newDef
+ });
}
exclude(values) {
- return ZodEnum.create(this.options.filter(opt => !values.includes(opt)));
+ let newDef = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this._def;
+ return ZodEnum.create(this.options.filter(opt => !values.includes(opt)), {
+ ...this._def,
+ ...newDef
+ });
}
}
exports.ZodEnum = ZodEnum;
+_ZodEnum_cache = new WeakMap();
ZodEnum.create = createZodEnum;
class ZodNativeEnum extends ZodType {
+ constructor() {
+ super(...arguments);
+ _ZodNativeEnum_cache.set(this, void 0);
+ }
_parse(input) {
const nativeEnumValues = util_1.util.getValidEnumValues(this._def.values);
const ctx = this._getOrReturnCtx(input);
@@ -3580,7 +3765,10 @@ class ZodNativeEnum extends ZodType {
});
return parseUtil_1.INVALID;
}
- if (nativeEnumValues.indexOf(input.data) === -1) {
+ if (!__classPrivateFieldGet(this, _ZodNativeEnum_cache, "f")) {
+ __classPrivateFieldSet(this, _ZodNativeEnum_cache, new Set(util_1.util.getValidEnumValues(this._def.values)), "f");
+ }
+ if (!__classPrivateFieldGet(this, _ZodNativeEnum_cache, "f").has(input.data)) {
const expectedValues = util_1.util.objectValues(nativeEnumValues);
(0, parseUtil_1.addIssueToContext)(ctx, {
received: ctx.data,
@@ -3596,6 +3784,7 @@ class ZodNativeEnum extends ZodType {
}
}
exports.ZodNativeEnum = ZodNativeEnum;
+_ZodNativeEnum_cache = new WeakMap();
ZodNativeEnum.create = (values, params) => {
return new ZodNativeEnum({
values: values,
@@ -3665,32 +3854,34 @@ class ZodEffects extends ZodType {
checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx);
if (effect.type === "preprocess") {
const processed = effect.transform(ctx.data, checkCtx);
- if (ctx.common.issues.length) {
- return {
- status: "dirty",
- value: ctx.data
- };
- }
if (ctx.common.async) {
- return Promise.resolve(processed).then(processed => {
- return this._def.schema._parseAsync({
+ return Promise.resolve(processed).then(async processed => {
+ if (status.value === "aborted") return parseUtil_1.INVALID;
+ const result = await this._def.schema._parseAsync({
data: processed,
path: ctx.path,
parent: ctx
});
+ if (result.status === "aborted") return parseUtil_1.INVALID;
+ if (result.status === "dirty") return (0, parseUtil_1.DIRTY)(result.value);
+ if (status.value === "dirty") return (0, parseUtil_1.DIRTY)(result.value);
+ return result;
});
} else {
- return this._def.schema._parseSync({
+ if (status.value === "aborted") return parseUtil_1.INVALID;
+ const result = this._def.schema._parseSync({
data: processed,
path: ctx.path,
parent: ctx
});
+ if (result.status === "aborted") return parseUtil_1.INVALID;
+ if (result.status === "dirty") return (0, parseUtil_1.DIRTY)(result.value);
+ if (status.value === "dirty") return (0, parseUtil_1.DIRTY)(result.value);
+ return result;
}
}
if (effect.type === "refinement") {
- const executeRefinement = (acc
- // effect: RefinementEffect
- ) => {
+ const executeRefinement = acc => {
const result = effect.refinement(acc, checkCtx);
if (ctx.common.async) {
return Promise.resolve(result);
@@ -4013,10 +4204,16 @@ exports.ZodPipeline = ZodPipeline;
class ZodReadonly extends ZodType {
_parse(input) {
const result = this._def.innerType._parse(input);
- if ((0, parseUtil_1.isValid)(result)) {
- result.value = Object.freeze(result.value);
- }
- return result;
+ const freeze = data => {
+ if ((0, parseUtil_1.isValid)(data)) {
+ data.value = Object.freeze(data.value);
+ }
+ return data;
+ };
+ return (0, parseUtil_1.isAsync)(result) ? result.then(data => freeze(data)) : freeze(result);
+ }
+ unwrap() {
+ return this._def.innerType;
}
}
exports.ZodReadonly = ZodReadonly;
@@ -4027,7 +4224,7 @@ ZodReadonly.create = (type, params) => {
...processCreateParams(params)
});
};
-const custom = function (check) {
+function custom(check) {
let params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
let
/**
@@ -4059,7 +4256,7 @@ const custom = function (check) {
}
});
return ZodAny.create();
-};
+}
exports.custom = custom;
exports.late = {
object: ZodObject.lazycreate
@@ -4113,7 +4310,7 @@ cls) {
let params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
message: `Input not instance of ${cls.name}`
};
- return (0, exports.custom)(data => data instanceof cls, params);
+ return custom(data => data instanceof cls, params);
};
exports.instanceof = instanceOfType;
const stringType = ZodString.create;
@@ -4435,8 +4632,8 @@ class SchemaValidationError extends Error {
}
case 'invalid_union':
{
- for (let unionError of issue.unionErrors) {
- for (let issue1 of unionError.issues) {
+ for (const unionError of issue.unionErrors) {
+ for (const issue1 of unionError.issues) {
log(issue1);
}
}
@@ -4448,7 +4645,7 @@ class SchemaValidationError extends Error {
}
}
}
- for (let error of errors) {
+ for (const error of errors) {
log(error);
}
const message = [heading, 'please see the details above'].join('\n ');
@@ -4571,8 +4768,8 @@ class DeviceApi {
*/
async request(deviceApiCall, options) {
deviceApiCall.validateParams();
- let result = await this.transport.send(deviceApiCall, options);
- let processed = deviceApiCall.preResultValidation(result);
+ const result = await this.transport.send(deviceApiCall, options);
+ const processed = deviceApiCall.preResultValidation(result);
return deviceApiCall.validateResult(processed);
}
/**
@@ -4660,44 +4857,44 @@ var _webkit = require("./webkit.js");
*/
class Messaging {
/**
- * @param {WebkitMessagingConfig} config
- */
+ * @param {WebkitMessagingConfig} config
+ */
constructor(config) {
this.transport = getTransport(config);
}
/**
- * Send a 'fire-and-forget' message.
- * @throws {Error}
- * {@link MissingHandler}
- *
- * @example
- *
- * ```
- * const messaging = new Messaging(config)
- * messaging.notify("foo", {bar: "baz"})
- * ```
- * @param {string} name
- * @param {Record} [data]
- */
+ * Send a 'fire-and-forget' message.
+ * @throws {Error}
+ * {@link MissingHandler}
+ *
+ * @example
+ *
+ * ```
+ * const messaging = new Messaging(config)
+ * messaging.notify("foo", {bar: "baz"})
+ * ```
+ * @param {string} name
+ * @param {Record} [data]
+ */
notify(name) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
this.transport.notify(name, data);
}
/**
- * Send a request, and wait for a response
- * @throws {Error}
- * {@link MissingHandler}
- *
- * @example
- * ```
- * const messaging = new Messaging(config)
- * const response = await messaging.request("foo", {bar: "baz"})
- * ```
- *
- * @param {string} name
- * @param {Record} [data]
- * @return {Promise}
- */
+ * Send a request, and wait for a response
+ * @throws {Error}
+ * {@link MissingHandler}
+ *
+ * @example
+ * ```
+ * const messaging = new Messaging(config)
+ * const response = await messaging.request("foo", {bar: "baz"})
+ * ```
+ *
+ * @param {string} name
+ * @param {Record} [data]
+ * @return {Promise}
+ */
request(name) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
return this.transport.request(name, data);
@@ -4710,20 +4907,20 @@ class Messaging {
exports.Messaging = Messaging;
class MessagingTransport {
/**
- * @param {string} name
- * @param {Record} [data]
- * @returns {void}
- */
+ * @param {string} name
+ * @param {Record} [data]
+ * @returns {void}
+ */
// @ts-ignore - ignoring a no-unused ts error, this is only an interface.
notify(name) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
throw new Error("must implement 'notify'");
}
/**
- * @param {string} name
- * @param {Record} [data]
- * @return {Promise}
- */
+ * @param {string} name
+ * @param {Record} [data]
+ * @return {Promise}
+ */
// @ts-ignore - ignoring a no-unused ts error, this is only an interface.
request(name) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -4748,9 +4945,9 @@ function getTransport(config) {
*/
class MissingHandler extends Error {
/**
- * @param {string} message
- * @param {string} handlerName
- */
+ * @param {string} message
+ * @param {string} handlerName
+ */
constructor(message, handlerName) {
super(message);
this.handlerName = handlerName;
@@ -4856,8 +5053,8 @@ class WebkitMessagingTransport {
config;
globals;
/**
- * @param {WebkitMessagingConfig} config
- */
+ * @param {WebkitMessagingConfig} config
+ */
constructor(config) {
this.config = config;
this.globals = captureGlobals();
@@ -4866,11 +5063,11 @@ class WebkitMessagingTransport {
}
}
/**
- * Sends message to the webkit layer (fire and forget)
- * @param {String} handler
- * @param {*} data
- * @internal
- */
+ * Sends message to the webkit layer (fire and forget)
+ * @param {String} handler
+ * @param {*} data
+ * @internal
+ */
wkSend(handler) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
if (!(handler in this.globals.window.webkit.messageHandlers)) {
@@ -4894,12 +5091,12 @@ class WebkitMessagingTransport {
}
/**
- * Sends message to the webkit layer and waits for the specified response
- * @param {String} handler
- * @param {*} data
- * @returns {Promise<*>}
- * @internal
- */
+ * Sends message to the webkit layer and waits for the specified response
+ * @param {String} handler
+ * @param {*} data
+ * @returns {Promise<*>}
+ * @internal
+ */
async wkSendAndWait(handler) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
if (this.config.hasModernWebkitAPI) {
@@ -4940,27 +5137,27 @@ class WebkitMessagingTransport {
}
}
/**
- * @param {string} name
- * @param {Record} [data]
- */
+ * @param {string} name
+ * @param {Record} [data]
+ */
notify(name) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
this.wkSend(name, data);
}
/**
- * @param {string} name
- * @param {Record} [data]
- */
+ * @param {string} name
+ * @param {Record} [data]
+ */
request(name) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
return this.wkSendAndWait(name, data);
}
/**
- * Generate a random method name and adds it to the global scope
- * The native layer will use this method to send the response
- * @param {string | number} randomMethodName
- * @param {Function} callback
- */
+ * Generate a random method name and adds it to the global scope
+ * The native layer will use this method to send the response
+ * @param {string | number} randomMethodName
+ * @param {Function} callback
+ */
generateRandomMethod(randomMethodName, callback) {
var _this = this;
this.globals.ObjectDefineProperty(this.globals.window, randomMethodName, {
@@ -4969,8 +5166,8 @@ class WebkitMessagingTransport {
configurable: true,
writable: false,
/**
- * @param {any[]} args
- */
+ * @param {any[]} args
+ */
value: function () {
callback(...arguments);
// @ts-ignore - we want this to throw if it fails as it would indicate a fatal error.
@@ -4986,16 +5183,16 @@ class WebkitMessagingTransport {
}
/**
- * @type {{name: string, length: number}}
- */
+ * @type {{name: string, length: number}}
+ */
algoObj = {
name: 'AES-GCM',
length: 256
};
/**
- * @returns {Promise}
- */
+ * @returns {Promise}
+ */
async createRandKey() {
const key = await this.globals.generateKey(this.algoObj, true, ['encrypt', 'decrypt']);
const exportedKey = await this.globals.exportKey('raw', key);
@@ -5003,44 +5200,44 @@ class WebkitMessagingTransport {
}
/**
- * @returns {Uint8Array}
- */
+ * @returns {Uint8Array}
+ */
createRandIv() {
return this.globals.getRandomValues(new this.globals.Uint8Array(12));
}
/**
- * @param {BufferSource} ciphertext
- * @param {BufferSource} key
- * @param {Uint8Array} iv
- * @returns {Promise}
- */
+ * @param {BufferSource} ciphertext
+ * @param {BufferSource} key
+ * @param {Uint8Array} iv
+ * @returns {Promise}
+ */
async decrypt(ciphertext, key, iv) {
const cryptoKey = await this.globals.importKey('raw', key, 'AES-GCM', false, ['decrypt']);
const algo = {
name: 'AES-GCM',
iv
};
- let decrypted = await this.globals.decrypt(algo, cryptoKey, ciphertext);
- let dec = new this.globals.TextDecoder();
+ const decrypted = await this.globals.decrypt(algo, cryptoKey, ciphertext);
+ const dec = new this.globals.TextDecoder();
return dec.decode(decrypted);
}
/**
- * When required (such as on macos 10.x), capture the `postMessage` method on
- * each webkit messageHandler
- *
- * @param {string[]} handlerNames
- */
+ * When required (such as on macos 10.x), capture the `postMessage` method on
+ * each webkit messageHandler
+ *
+ * @param {string[]} handlerNames
+ */
captureWebkitHandlers(handlerNames) {
const handlers = window.webkit.messageHandlers;
if (!handlers) throw new _messaging.MissingHandler('window.webkit.messageHandlers was absent', 'all');
- for (let webkitMessageHandlerName of handlerNames) {
+ for (const webkitMessageHandlerName of handlerNames) {
if (typeof handlers[webkitMessageHandlerName]?.postMessage === 'function') {
/**
- * `bind` is used here to ensure future calls to the captured
- * `postMessage` have the correct `this` context
- */
+ * `bind` is used here to ensure future calls to the captured
+ * `postMessage` have the correct `this` context
+ */
const original = handlers[webkitMessageHandlerName];
const bound = handlers[webkitMessageHandlerName].postMessage?.bind(original);
this.globals.capturedWebkitHandlers[webkitMessageHandlerName] = bound;
@@ -5069,11 +5266,11 @@ class WebkitMessagingTransport {
exports.WebkitMessagingTransport = WebkitMessagingTransport;
class WebkitMessagingConfig {
/**
- * @param {object} params
- * @param {boolean} params.hasModernWebkitAPI
- * @param {string[]} params.webkitMessageHandlerNames
- * @param {string} params.secret
- */
+ * @param {object} params
+ * @param {boolean} params.hasModernWebkitAPI
+ * @param {string[]} params.webkitMessageHandlerNames
+ * @param {string} params.secret
+ */
constructor(params) {
/**
* Whether or not the current WebKit Platform supports secure messaging
@@ -5081,13 +5278,13 @@ class WebkitMessagingConfig {
*/
this.hasModernWebkitAPI = params.hasModernWebkitAPI;
/**
- * A list of WebKit message handler names that a user script can send
- */
+ * A list of WebKit message handler names that a user script can send
+ */
this.webkitMessageHandlerNames = params.webkitMessageHandlerNames;
/**
- * A string provided by native platforms to be sent with future outgoing
- * messages
- */
+ * A string provided by native platforms to be sent with future outgoing
+ * messages
+ */
this.secret = params.secret;
}
}
@@ -5099,28 +5296,28 @@ class WebkitMessagingConfig {
exports.WebkitMessagingConfig = WebkitMessagingConfig;
class SecureMessagingParams {
/**
- * @param {object} params
- * @param {string} params.methodName
- * @param {string} params.secret
- * @param {number[]} params.key
- * @param {number[]} params.iv
- */
+ * @param {object} params
+ * @param {string} params.methodName
+ * @param {string} params.secret
+ * @param {number[]} params.key
+ * @param {number[]} params.iv
+ */
constructor(params) {
/**
* The method that's been appended to `window` to be called later
*/
this.methodName = params.methodName;
/**
- * The secret used to ensure message sender validity
- */
+ * The secret used to ensure message sender validity
+ */
this.secret = params.secret;
/**
- * The CipherKey as number[]
- */
+ * The CipherKey as number[]
+ */
this.key = params.key;
/**
- * The Initial Vector as number[]
- */
+ * The Initial Vector as number[]
+ */
this.iv = params.iv;
}
}
@@ -5331,7 +5528,7 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj &&
* }} PasswordParameters
*/
const defaults = Object.freeze({
- SCAN_SET_ORDER: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-~!@#$%^&*_+=`|(){}[:;\\\"'<>,.?/ ]",
+ SCAN_SET_ORDER: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-~!@#$%^&*_+=`|(){}[:;\\"\'<>,.?/ ]',
defaultUnambiguousCharacters: 'abcdefghijkmnopqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ0123456789',
defaultPasswordLength: _constants.constants.DEFAULT_MIN_LENGTH,
defaultPasswordRules: _constants.constants.DEFAULT_PASSWORD_RULES,
@@ -5458,7 +5655,7 @@ class Password {
_requirementsFromRules(passwordRules) {
/** @type {Requirements} */
const requirements = {};
- for (let rule of passwordRules) {
+ for (const rule of passwordRules) {
if (rule.name === parser.RuleName.ALLOWED) {
console.assert(!('PasswordAllowedCharacters' in requirements));
const chars = this._charactersFromCharactersClasses(rule.value);
@@ -5789,7 +5986,7 @@ class Password {
*/
_charactersFromCharactersClasses(characterClasses) {
const output = [];
- for (let characterClass of characterClasses) {
+ for (const characterClass of characterClasses) {
output.push(...this._scanSetFromCharacterClass(characterClass));
}
return output;
@@ -5803,9 +6000,9 @@ class Password {
if (!characters.length) {
return '';
}
- let shadowCharacters = Array.prototype.slice.call(characters);
+ const shadowCharacters = Array.prototype.slice.call(characters);
shadowCharacters.sort((a, b) => this.options.SCAN_SET_ORDER.indexOf(a) - this.options.SCAN_SET_ORDER.indexOf(b));
- let uniqueCharacters = [shadowCharacters[0]];
+ const uniqueCharacters = [shadowCharacters[0]];
for (let i = 1, length = shadowCharacters.length; i < length; ++i) {
if (shadowCharacters[i] === shadowCharacters[i - 1]) {
continue;
@@ -5845,6 +6042,7 @@ Object.defineProperty(exports, "__esModule", {
});
exports.SHOULD_NOT_BE_REACHED = exports.RuleName = exports.Rule = exports.ParserError = exports.NamedCharacterClass = exports.Identifier = exports.CustomCharacterClass = void 0;
exports.parsePasswordRules = parsePasswordRules;
+/* eslint-disable no-var */
// Copyright (c) 2019 - 2020 Apple Inc. Licensed under MIT License.
/*
@@ -5897,7 +6095,6 @@ class Rule {
}
}
exports.Rule = Rule;
-;
class NamedCharacterClass {
constructor(name) {
console.assert(_isValidRequiredOrAllowedPropertyValueIdentifier(name));
@@ -5914,10 +6111,8 @@ class NamedCharacterClass {
}
}
exports.NamedCharacterClass = NamedCharacterClass;
-;
class ParserError extends Error {}
exports.ParserError = ParserError;
-;
class CustomCharacterClass {
constructor(characters) {
console.assert(characters instanceof Array);
@@ -5933,14 +6128,11 @@ class CustomCharacterClass {
return `[${this._characters.join('').replace('"', '"')}]`;
}
}
-exports.CustomCharacterClass = CustomCharacterClass;
-;
// MARK: Lexer functions
-
+exports.CustomCharacterClass = CustomCharacterClass;
function _isIdentifierCharacter(c) {
console.assert(c.length === 1);
- // eslint-disable-next-line no-mixed-operators
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c === '-';
}
function _isASCIIDigit(c) {
@@ -5986,14 +6178,14 @@ function _markBitsForNamedCharacterClass(bitSet, namedCharacterClass) {
}
}
function _markBitsForCustomCharacterClass(bitSet, customCharacterClass) {
- for (let character of customCharacterClass.characters) {
+ for (const character of customCharacterClass.characters) {
bitSet[_bitSetIndexForCharacter(character)] = true;
}
}
function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFormatCompliant) {
// @ts-ignore
- let asciiPrintableBitSet = new Array('~'.codePointAt(0) - ' '.codePointAt(0) + 1);
- for (let propertyValue of propertyValues) {
+ const asciiPrintableBitSet = new Array('~'.codePointAt(0) - ' '.codePointAt(0) + 1);
+ for (const propertyValue of propertyValues) {
if (propertyValue instanceof NamedCharacterClass) {
if (propertyValue.name === Identifier.UNICODE) {
return [new NamedCharacterClass(Identifier.UNICODE)];
@@ -6008,32 +6200,32 @@ function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFo
}
let charactersSeen = [];
function checkRange(start, end) {
- let temp = [];
+ const temp = [];
for (let i = _bitSetIndexForCharacter(start); i <= _bitSetIndexForCharacter(end); ++i) {
if (asciiPrintableBitSet[i]) {
temp.push(_characterAtBitSetIndex(i));
}
}
- let result = temp.length === _bitSetIndexForCharacter(end) - _bitSetIndexForCharacter(start) + 1;
+ const result = temp.length === _bitSetIndexForCharacter(end) - _bitSetIndexForCharacter(start) + 1;
if (!result) {
charactersSeen = charactersSeen.concat(temp);
}
return result;
}
- let hasAllUpper = checkRange('A', 'Z');
- let hasAllLower = checkRange('a', 'z');
- let hasAllDigits = checkRange('0', '9');
+ const hasAllUpper = checkRange('A', 'Z');
+ const hasAllLower = checkRange('a', 'z');
+ const hasAllDigits = checkRange('0', '9');
// Check for special characters, accounting for characters that are given special treatment (i.e. '-' and ']')
let hasAllSpecial = false;
let hasDash = false;
let hasRightSquareBracket = false;
- let temp = [];
+ const temp = [];
for (let i = _bitSetIndexForCharacter(' '); i <= _bitSetIndexForCharacter('/'); ++i) {
if (!asciiPrintableBitSet[i]) {
continue;
}
- let character = _characterAtBitSetIndex(i);
+ const character = _characterAtBitSetIndex(i);
if (keepCustomCharacterClassFormatCompliant && character === '-') {
hasDash = true;
} else {
@@ -6049,7 +6241,7 @@ function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFo
if (!asciiPrintableBitSet[i]) {
continue;
}
- let character = _characterAtBitSetIndex(i);
+ const character = _characterAtBitSetIndex(i);
if (keepCustomCharacterClassFormatCompliant && character === ']') {
hasRightSquareBracket = true;
} else {
@@ -6067,12 +6259,12 @@ function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFo
if (hasRightSquareBracket) {
temp.push(']');
}
- let numberOfSpecialCharacters = _bitSetIndexForCharacter('/') - _bitSetIndexForCharacter(' ') + 1 + (_bitSetIndexForCharacter('@') - _bitSetIndexForCharacter(':') + 1) + (_bitSetIndexForCharacter('`') - _bitSetIndexForCharacter('[') + 1) + (_bitSetIndexForCharacter('~') - _bitSetIndexForCharacter('{') + 1);
+ const numberOfSpecialCharacters = _bitSetIndexForCharacter('/') - _bitSetIndexForCharacter(' ') + 1 + (_bitSetIndexForCharacter('@') - _bitSetIndexForCharacter(':') + 1) + (_bitSetIndexForCharacter('`') - _bitSetIndexForCharacter('[') + 1) + (_bitSetIndexForCharacter('~') - _bitSetIndexForCharacter('{') + 1);
hasAllSpecial = temp.length === numberOfSpecialCharacters;
if (!hasAllSpecial) {
charactersSeen = charactersSeen.concat(temp);
}
- let result = [];
+ const result = [];
if (hasAllUpper && hasAllLower && hasAllDigits && hasAllSpecial) {
return [new NamedCharacterClass(Identifier.ASCII_PRINTABLE)];
}
@@ -6100,7 +6292,7 @@ function _indexOfNonWhitespaceCharacter(input) {
let position = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
console.assert(position >= 0);
console.assert(position <= input.length);
- let length = input.length;
+ const length = input.length;
while (position < length && _isASCIIWhitespace(input[position])) {
++position;
}
@@ -6110,10 +6302,10 @@ function _parseIdentifier(input, position) {
console.assert(position >= 0);
console.assert(position < input.length);
console.assert(_isIdentifierCharacter(input[position]));
- let length = input.length;
- let seenIdentifiers = [];
+ const length = input.length;
+ const seenIdentifiers = [];
do {
- let c = input[position];
+ const c = input[position];
if (!_isIdentifierCharacter(c)) {
break;
}
@@ -6129,16 +6321,16 @@ function _parseCustomCharacterClass(input, position) {
console.assert(position >= 0);
console.assert(position < input.length);
console.assert(input[position] === CHARACTER_CLASS_START_SENTINEL);
- let length = input.length;
+ const length = input.length;
++position;
if (position >= length) {
// console.error('Found end-of-line instead of character class character')
return [null, position];
}
- let initialPosition = position;
- let result = [];
+ const initialPosition = position;
+ const result = [];
do {
- let c = input[position];
+ const c = input[position];
if (!_isASCIIPrintableCharacter(c)) {
++position;
continue;
@@ -6174,11 +6366,11 @@ function _parseCustomCharacterClass(input, position) {
function _parsePasswordRequiredOrAllowedPropertyValue(input, position) {
console.assert(position >= 0);
console.assert(position < input.length);
- let length = input.length;
- let propertyValues = [];
+ const length = input.length;
+ const propertyValues = [];
while (true) {
if (_isIdentifierCharacter(input[position])) {
- let identifierStartPosition = position;
+ const identifierStartPosition = position;
// eslint-disable-next-line no-redeclare
var [propertyValue, position] = _parseIdentifier(input, position);
if (!_isValidRequiredOrAllowedPropertyValueIdentifier(propertyValue)) {
@@ -6225,8 +6417,8 @@ function _parsePasswordRule(input, position) {
console.assert(position >= 0);
console.assert(position < input.length);
console.assert(_isIdentifierCharacter(input[position]));
- let length = input.length;
- var mayBeIdentifierStartPosition = position;
+ const length = input.length;
+ const mayBeIdentifierStartPosition = position;
// eslint-disable-next-line no-redeclare
var [identifier, position] = _parseIdentifier(input, position);
if (!Object.values(RuleName).includes(identifier)) {
@@ -6241,7 +6433,7 @@ function _parsePasswordRule(input, position) {
// console.error('Failed to find start of property value: ' + input.substr(position))
return [null, position, undefined];
}
- let property = {
+ const property = {
name: identifier,
value: null
};
@@ -6297,7 +6489,7 @@ function _parseInteger(input, position) {
// console.error('Failed to parse value of type integer; not a number: ' + input.substr(position))
return [null, position];
}
- let length = input.length;
+ const length = input.length;
// let initialPosition = position
let result = 0;
do {
@@ -6318,8 +6510,8 @@ function _parseInteger(input, position) {
* @private
*/
function _parsePasswordRulesInternal(input) {
- let parsedProperties = [];
- let length = input.length;
+ const parsedProperties = [];
+ const length = input.length;
var position = _indexOfNonWhitespaceCharacter(input);
while (position < length) {
if (!_isIdentifierCharacter(input[position])) {
@@ -6356,7 +6548,7 @@ function _parsePasswordRulesInternal(input) {
* @returns {Rule[]}
*/
function parsePasswordRules(input, formatRulesForMinifiedVersion) {
- let [passwordRules, maybeMessage] = _parsePasswordRulesInternal(input);
+ const [passwordRules, maybeMessage] = _parsePasswordRulesInternal(input);
if (!passwordRules) {
throw new ParserError(maybeMessage);
}
@@ -6366,13 +6558,13 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) {
// When formatting rules for minified version, we should keep the formatted rules
// as similar to the input as possible. Avoid copying required rules to allowed rules.
- let suppressCopyingRequiredToAllowed = formatRulesForMinifiedVersion;
- let requiredRules = [];
+ const suppressCopyingRequiredToAllowed = formatRulesForMinifiedVersion;
+ const requiredRules = [];
let newAllowedValues = [];
let minimumMaximumConsecutiveCharacters = null;
let maximumMinLength = 0;
let minimumMaxLength = null;
- for (let rule of passwordRules) {
+ for (const rule of passwordRules) {
switch (rule.name) {
case RuleName.MAX_CONSECUTIVE:
minimumMaximumConsecutiveCharacters = minimumMaximumConsecutiveCharacters ? Math.min(rule.value, minimumMaximumConsecutiveCharacters) : rule.value;
@@ -6405,10 +6597,10 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) {
if (minimumMaximumConsecutiveCharacters !== null) {
newPasswordRules.push(new Rule(RuleName.MAX_CONSECUTIVE, minimumMaximumConsecutiveCharacters));
}
- let sortedRequiredRules = requiredRules.sort(function (a, b) {
+ const sortedRequiredRules = requiredRules.sort(function (a, b) {
const namedCharacterClassOrder = [Identifier.LOWER, Identifier.UPPER, Identifier.DIGIT, Identifier.SPECIAL, Identifier.ASCII_PRINTABLE, Identifier.UNICODE];
- let aIsJustOneNamedCharacterClass = a.value.length === 1 && a.value[0] instanceof NamedCharacterClass;
- let bIsJustOneNamedCharacterClass = b.value.length === 1 && b.value[0] instanceof NamedCharacterClass;
+ const aIsJustOneNamedCharacterClass = a.value.length === 1 && a.value[0] instanceof NamedCharacterClass;
+ const bIsJustOneNamedCharacterClass = b.value.length === 1 && b.value[0] instanceof NamedCharacterClass;
if (aIsJustOneNamedCharacterClass && !bIsJustOneNamedCharacterClass) {
return -1;
}
@@ -6416,8 +6608,8 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) {
return 1;
}
if (aIsJustOneNamedCharacterClass && bIsJustOneNamedCharacterClass) {
- let aIndex = namedCharacterClassOrder.indexOf(a.value[0].name);
- let bIndex = namedCharacterClassOrder.indexOf(b.value[0].name);
+ const aIndex = namedCharacterClassOrder.indexOf(a.value[0].name);
+ const bIndex = namedCharacterClassOrder.indexOf(b.value[0].name);
return aIndex - bIndex;
}
return 0;
@@ -7062,6 +7254,9 @@ module.exports={
"keldoc.com": {
"password-rules": "minlength: 12; required: lower; required: upper; required: digit; required: [!@#$%^&*];"
},
+ "kennedy-center.org": {
+ "password-rules": "minlength: 8; required: lower; required: upper; required: digit; required: [!#$%&*?@];"
+ },
"key.harvard.edu": {
"password-rules": "minlength: 10; maxlength: 100; required: lower; required: upper; required: digit; allowed: [-@_#!&$`%*+()./,;~:{}|?>=<^[']];"
},
@@ -7574,8 +7769,8 @@ class CredentialsImport {
activeInput?.focus();
}
async started() {
- this.device.deviceApi.notify(new _deviceApiCalls.CloseAutofillParentCall(null));
this.device.deviceApi.notify(new _deviceApiCalls.StartCredentialsImportFlowCall({}));
+ this.device.deviceApi.notify(new _deviceApiCalls.CloseAutofillParentCall(null));
}
async dismissed() {
this.device.deviceApi.notify(new _deviceApiCalls.CredentialsImportFlowPermanentlyDismissedCall(null));
@@ -7619,7 +7814,7 @@ function createDevice() {
};
// Create the DeviceAPI + Setting
- let deviceApi = new _index.DeviceApi(globalConfig.isDDGTestMode ? loggingTransport : transport);
+ const deviceApi = new _index.DeviceApi(globalConfig.isDDGTestMode ? loggingTransport : transport);
const settings = new _Settings.Settings(globalConfig, deviceApi);
if (globalConfig.isWindows) {
if (globalConfig.isTopFrame) {
@@ -7753,9 +7948,9 @@ class AndroidInterface extends _InterfacePrototype.default {
}
/**
- * Used by the email web app
- * Provides functionality to log the user out
- */
+ * Used by the email web app
+ * Provides functionality to log the user out
+ */
removeUserData() {
try {
return window.EmailInterface.removeCredentials();
@@ -9024,14 +9219,16 @@ class InterfacePrototype {
});
break;
default:
- // Also fire pixel when filling an identity with the personal duck address from an email field
- const checks = [subtype === 'emailAddress', this.hasLocalAddresses, data?.emailAddress === (0, _autofillUtils.formatDuckAddress)(this.#addresses.personalAddress)];
- if (checks.every(Boolean)) {
- this.firePixel({
- pixelName: 'autofill_personal_address'
- });
+ {
+ // Also fire pixel when filling an identity with the personal duck address from an email field
+ const checks = [subtype === 'emailAddress', this.hasLocalAddresses, data?.emailAddress === (0, _autofillUtils.formatDuckAddress)(this.#addresses.personalAddress)];
+ if (checks.every(Boolean)) {
+ this.firePixel({
+ pixelName: 'autofill_personal_address'
+ });
+ }
+ break;
}
- break;
}
}
// some platforms do not include a `success` object, why?
@@ -9279,13 +9476,15 @@ class InterfacePrototype {
postSubmit(values, form) {
if (!form.form) return;
if (!form.hasValues(values)) return;
- const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated];
+ const shouldTriggerPartialSave = Object.keys(values?.credentials || {}).length === 1 && Boolean(values?.credentials?.username) && this.settings.featureToggles.partial_form_saves;
+ const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, shouldTriggerPartialSave];
if (checks.some(Boolean)) {
const formData = (0, _Credentials.appendGeneratedKey)(values, {
password: this.passwordGenerator.password,
username: this.emailProtection.lastGenerated
});
- this.storeFormData(formData, 'formSubmission');
+ const trigger = shouldTriggerPartialSave ? 'partialSave' : 'formSubmission';
+ this.storeFormData(formData, trigger);
}
}
@@ -9706,6 +9905,7 @@ function initFormSubmissionsApi(forms, matching) {
// @ts-ignore
if (btns.find(btn => btn.contains(realTarget))) return true;
+ return false;
});
matchingForm?.submitHandler('global pointerdown event + matching form');
if (!matchingForm) {
@@ -9801,7 +10001,7 @@ function overlayApi(device) {
* @returns {Promise}
*/
async selectedDetail(data, type) {
- let detailsEntries = Object.entries(data).map(_ref => {
+ const detailsEntries = Object.entries(data).map(_ref => {
let [key, value] = _ref;
return [key, String(value)];
});
@@ -10064,7 +10264,7 @@ class Form {
*/
getValuesReadyForStorage() {
const formValues = this.getRawValues();
- return (0, _formatters.prepareFormValuesForStorage)(formValues);
+ return (0, _formatters.prepareFormValuesForStorage)(formValues, this.device.settings.featureToggles.partial_form_saves);
}
/**
@@ -10095,7 +10295,7 @@ class Form {
if (!input.classList.contains('ddg-autofilled')) return;
(0, _autofillUtils.removeInlineStyles)(input, (0, _inputStyles.getIconStylesAutofilled)(input, this));
(0, _autofillUtils.removeInlineStyles)(input, {
- 'cursor': 'pointer'
+ cursor: 'pointer'
});
input.classList.remove('ddg-autofilled');
this.addAutofillStyles(input);
@@ -10216,20 +10416,10 @@ class Form {
if (this.form.matches(selector)) {
this.addInput(this.form);
} else {
- /** @type {Element[] | NodeList} */
- let foundInputs = [];
- // Some sites seem to be overriding `form.elements`, so we need to check if it's still iterable.
- if (this.form instanceof HTMLFormElement && this.form.elements != null && Symbol.iterator in Object(this.form.elements)) {
- // For form elements we use .elements to catch fields outside the form itself using the form attribute.
- // It also catches all elements when the markup is broken.
- // We use .filter to avoid fieldset, button, textarea etc.
- const formElements = [...this.form.elements].filter(el => el.matches(selector));
- // If there are no form elements, we try to look for all
- // enclosed elements within the form.
- foundInputs = formElements.length > 0 ? formElements : (0, _autofillUtils.findEnclosedElements)(this.form, selector);
- } else {
- foundInputs = this.form.querySelectorAll(selector);
- }
+ // Attempt to get form's control elements first as it can catch elements when markup is broke, or if the fields are outside the form.
+ // Other wise use queryElementsWithShadow, that can scan for shadow tree.
+ const formControlElements = (0, _autofillUtils.getFormControlElements)(this.form, selector);
+ const foundInputs = formControlElements != null ? [...formControlElements, ...(0, _autofillUtils.findElementsInShadowTree)(this.form, selector)] : (0, _autofillUtils.queryElementsWithShadow)(this.form, selector, true);
if (foundInputs.length < MAX_INPUTS_PER_FORM) {
foundInputs.forEach(input => this.addInput(input));
} else {
@@ -10290,7 +10480,7 @@ class Form {
}
get submitButtons() {
const selector = this.matching.cssSelector('submitButtonSelector');
- const allButtons = /** @type {HTMLElement[]} */(0, _autofillUtils.findEnclosedElements)(this.form, selector);
+ const allButtons = /** @type {HTMLElement[]} */(0, _autofillUtils.queryElementsWithShadow)(this.form, selector);
return allButtons.filter(btn => (0, _autofillUtils.isPotentiallyViewable)(btn) && (0, _autofillUtils.isLikelyASubmitButton)(btn, this.matching) && (0, _autofillUtils.buttonMatchesFormType)(btn, this));
}
attemptSubmissionIfNeeded() {
@@ -10413,12 +10603,12 @@ class Form {
if ((0, _autofillUtils.wasAutofilledByChrome)(input)) return;
if ((0, _autofillUtils.isEventWithinDax)(e, e.target)) {
(0, _autofillUtils.addInlineStyles)(e.target, {
- 'cursor': 'pointer',
+ cursor: 'pointer',
...onMouseMove
});
} else {
(0, _autofillUtils.removeInlineStyles)(e.target, {
- 'cursor': 'pointer'
+ cursor: 'pointer'
});
// Only overwrite active icon styles if tooltip is closed
if (!this.device.isTooltipActive()) {
@@ -10430,7 +10620,7 @@ class Form {
});
this.addListener(input, 'mouseleave', e => {
(0, _autofillUtils.removeInlineStyles)(e.target, {
- 'cursor': 'pointer'
+ cursor: 'pointer'
});
// Only overwrite active icon styles if tooltip is closed
if (!this.device.isTooltipActive()) {
@@ -10528,7 +10718,7 @@ class Form {
this.touched.add(input);
this.device.attachTooltip({
form: this,
- input: input,
+ input,
click: clickCoords,
trigger: 'userInitiated',
triggerMetaData: {
@@ -10757,7 +10947,7 @@ class Form {
}, 'credentials');
this.device.attachTooltip({
form: this,
- input: input,
+ input,
click: null,
trigger: 'autoprompt',
triggerMetaData: {
@@ -10992,6 +11182,23 @@ class FormAnalyzer {
}
});
}
+
+ /**
+ * Function that checks if the element is an external link or a custom web element that
+ * encapsulates a link.
+ * @param {any} el
+ * @returns {boolean}
+ */
+ isElementExternalLink(el) {
+ // Checks if the element is present in the cusotm elements registry and ends with a '-link' suffix.
+ // If it does, it checks if it contains an anchor element inside.
+ const tagName = el.nodeName.toLowerCase();
+ const isCustomWebElementLink = customElements?.get(tagName) != null && /-link$/.test(tagName) && (0, _autofillUtils.findElementsInShadowTree)(el, 'a').length > 0;
+
+ // if an external link matches one of the regexes, we assume the match is not pertinent to the current form
+ const isElementLink = el instanceof HTMLAnchorElement && el.href && el.getAttribute('href') !== '#' || (el.getAttribute('role') || '').toUpperCase() === 'LINK' || el.matches('button[class*=secondary]');
+ return isCustomWebElementLink || isElementLink;
+ }
evaluateElement(el) {
const string = (0, _autofillUtils.getTextShallow)(el);
if (el.matches(this.matching.cssSelector('password'))) {
@@ -11012,7 +11219,7 @@ class FormAnalyzer {
if (likelyASubmit) {
this.form.querySelectorAll('input[type=submit], button[type=submit]').forEach(submit => {
// If there is another element marked as submit and this is not, flip back to false
- if (el.type !== 'submit' && el !== submit) {
+ if (el.getAttribute('type') !== 'submit' && el !== submit) {
likelyASubmit = false;
}
});
@@ -11031,8 +11238,7 @@ class FormAnalyzer {
});
return;
}
- // if an external link matches one of the regexes, we assume the match is not pertinent to the current form
- if (el instanceof HTMLAnchorElement && el.href && el.getAttribute('href') !== '#' || (el.getAttribute('role') || '').toUpperCase() === 'LINK' || el.matches('button[class*=secondary]')) {
+ if (this.isElementExternalLink(el)) {
let shouldFlip = true;
let strength = 1;
// Don't flip forgotten password links
@@ -11051,9 +11257,10 @@ class FormAnalyzer {
});
} else {
// any other case
+ const isH1Element = el.tagName === 'H1';
this.updateSignal({
string,
- strength: 1,
+ strength: isH1Element ? 3 : 1,
signalType: `generic: ${string}`,
shouldCheckUnifiedForm: true
});
@@ -11071,7 +11278,7 @@ class FormAnalyzer {
// Check form contents (noisy elements are skipped with the safeUniversalSelector)
const selector = this.matching.cssSelector('safeUniversalSelector');
- const formElements = (0, _autofillUtils.findEnclosedElements)(this.form, selector);
+ const formElements = (0, _autofillUtils.queryElementsWithShadow)(this.form, selector);
for (let i = 0; i < formElements.length; i++) {
// Safety cutoff to avoid huge DOMs freezing the browser
if (i >= 200) break;
@@ -11131,7 +11338,7 @@ class FormAnalyzer {
}
// Match form textContent against common cc fields (includes hidden labels)
- const textMatches = formEl.textContent?.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig);
+ const textMatches = formEl.textContent?.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/gi);
// De-dupe matches to avoid counting the same element more than once
const deDupedMatches = new Set(textMatches?.map(match => match.toLowerCase()));
@@ -11450,7 +11657,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = {
Anguilla: 'AI',
Albania: 'AL',
Armenia: 'AM',
- 'Curaçao': 'CW',
+ Curaçao: 'CW',
Angola: 'AO',
Antarctica: 'AQ',
Argentina: 'AR',
@@ -11639,7 +11846,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = {
Paraguay: 'PY',
Qatar: 'QA',
'Outlying Oceania': 'QO',
- 'Réunion': 'RE',
+ Réunion: 'RE',
Zimbabwe: 'ZW',
Romania: 'RO',
Russia: 'SU',
@@ -11716,6 +11923,7 @@ Object.defineProperty(exports, "__esModule", {
exports.prepareFormValuesForStorage = exports.inferCountryCodeFromElement = exports.getUnifiedExpiryDate = exports.getMMAndYYYYFromString = exports.getCountryName = exports.getCountryDisplayName = exports.formatPhoneNumber = exports.formatFullName = exports.formatCCYear = void 0;
var _matching = require("./matching.js");
var _countryNames = require("./countryNames.js");
+var _autofillUtils = require("../autofill-utils.js");
// Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024
const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i;
// Matches 4 non-digit repeated characters (YYYY or AAAA) or 4 digits (2022)
@@ -11884,36 +12092,25 @@ const getMMAndYYYYFromString = expiration => {
};
/**
- * @param {InternalDataStorageObject} credentials
+ * @param {InternalDataStorageObject} data
* @return {boolean}
*/
exports.getMMAndYYYYFromString = getMMAndYYYYFromString;
-const shouldStoreCredentials = _ref3 => {
- let {
- credentials
- } = _ref3;
- return Boolean(credentials.password);
-};
-
-/**
- * @param {InternalDataStorageObject} credentials
- * @return {boolean}
- */
-const shouldStoreIdentities = _ref4 => {
+const shouldStoreIdentities = _ref3 => {
let {
identities
- } = _ref4;
+ } = _ref3;
return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity);
};
/**
- * @param {InternalDataStorageObject} credentials
+ * @param {InternalDataStorageObject} data
* @return {boolean}
*/
-const shouldStoreCreditCards = _ref5 => {
+const shouldStoreCreditCards = _ref4 => {
let {
creditCards
- } = _ref5;
+ } = _ref4;
if (!creditCards.cardNumber) return false;
if (creditCards.cardSecurityCode) return true;
// Some forms (Amazon) don't have the cvv, so we still save if there's the expiration
@@ -11936,7 +12133,8 @@ const formatPhoneNumber = phone => phone.replaceAll(/[^0-9|+]/g, '');
* @return {DataStorageObject}
*/
exports.formatPhoneNumber = formatPhoneNumber;
-const prepareFormValuesForStorage = formValues => {
+const prepareFormValuesForStorage = function (formValues) {
+ let canTriggerPartialSave = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
/** @type {Partial} */
let {
credentials,
@@ -11949,14 +12147,15 @@ const prepareFormValuesForStorage = formValues => {
creditCards.cardName = identities?.fullName || formatFullName(identities);
}
- /** Fixes for credentials **/
- // Don't store if there isn't enough data
- if (shouldStoreCredentials(formValues)) {
- // If we don't have a username to match a password, let's see if the email is available
- if (credentials.password && !credentials.username && identities.emailAddress) {
- credentials.username = identities.emailAddress;
- }
- } else {
+ /** Fixes for credentials */
+ // If we don't have a username to match a password, let's see if email or phone are available
+ if (credentials.password && !credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) {
+ // @ts-ignore - username will be likely undefined, but needs to be specifically assigned to a string value
+ credentials.username = identities.emailAddress || identities.phone;
+ }
+
+ // If there's no password, and we shouldn't trigger a partial save, let's discard the object
+ if (!credentials.password && !canTriggerPartialSave) {
credentials = undefined;
}
@@ -12012,7 +12211,7 @@ const prepareFormValuesForStorage = formValues => {
};
exports.prepareFormValuesForStorage = prepareFormValuesForStorage;
-},{"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){
+},{"../autofill-utils.js":64,"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -12055,7 +12254,7 @@ const getBasicStyles = (input, icon) => ({
'background-repeat': 'no-repeat',
'background-origin': 'content-box',
'background-image': `url(${icon})`,
- 'transition': 'background 0s'
+ transition: 'background 0s'
});
/**
@@ -12098,7 +12297,7 @@ const getIconStylesAutofilled = (input, form) => {
return {
...iconStyle,
'background-color': '#F8F498',
- 'color': '#333333'
+ color: '#333333'
};
};
exports.getIconStylesAutofilled = getIconStylesAutofilled;
@@ -12386,14 +12585,14 @@ const extractElementStrings = element => {
// only take the string when it's an explicit text node
if (el.nodeType === el.TEXT_NODE || !el.childNodes.length) {
- let trimmedText = (0, _matching.removeExcessWhitespace)(el.textContent);
+ const trimmedText = (0, _matching.removeExcessWhitespace)(el.textContent);
if (trimmedText) {
strings.add(trimmedText);
}
return;
}
- for (let node of el.childNodes) {
- let nodeType = node.nodeType;
+ for (const node of el.childNodes) {
+ const nodeType = node.nodeType;
if (nodeType !== node.ELEMENT_NODE && nodeType !== node.TEXT_NODE) {
continue;
}
@@ -12795,7 +12994,7 @@ const matchingConfiguration = exports.matchingConfiguration = {
match: /sign.?up|join|register|enroll|(create|new).+account|newsletter|subscri(be|ption)|settings|preferences|profile|update|iscri(viti|zione)|registra(ti|zione)|(?:nuovo|crea(?:zione)?) account|contatt(?:ac)?i|sottoscriv|sottoscrizione|impostazioni|preferenze|aggiorna|anmeld(en|ung)|registrier(en|ung)|neukunde|neuer (kunde|benutzer|nutzer)|registreren|eigenschappen|profiel|bijwerken|s.inscrire|inscription|s.abonner|abonnement|préférences|profil|créer un compte|regis(trarse|tro)|regÃstrate|inscr(ibirse|ipción|Ãbete)|crea(r cuenta)?|nueva cuenta|nuevo (cliente|usuario)|preferencias|perfil|lista de correo|registrer(a|ing)|(nytt|öppna) konto|nyhetsbrev|prenumer(era|ation)|kontakt|skapa|starta|inställningar|min (sida|kundvagn)|uppdatera/iu
},
resetPasswordLink: {
- match: /(forgot(ten)?|reset|don't remember) (your )?password|password forgotten|password dimenticata|reset(?:ta) password|recuper[ao] password|(vergessen|verloren|verlegt|wiederherstellen) passwort|wachtwoord (vergeten|reset)|(oublié|récupérer) ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)|re(iniciar|cuperar) (contraseña|clave)|olvid(ó su|aste tu|é mi) (contraseña|clave)|recordar( su)? (contraseña|clave)|glömt lösenord|återställ lösenord/iu
+ match: /(forgot(ten)?|reset|don't remember).?(your )?password|password forgotten|password dimenticata|reset(?:ta) password|recuper[ao] password|(vergessen|verloren|verlegt|wiederherstellen) passwort|wachtwoord (vergeten|reset)|(oublié|récupérer) ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)|re(iniciar|cuperar) (contraseña|clave)|olvid(ó su|aste tu|é mi) (contraseña|clave)|recordar( su)? (contraseña|clave)|glömt lösenord|återställ lösenord/iu
},
loginProvidersRegex: {
match: / with | con | mit | met | avec /iu
@@ -13060,8 +13259,8 @@ class Matching {
*
* `email: [{type: "email", strategies: {cssSelector: "email", ... etc}]`
*/
- for (let [listName, matcherNames] of Object.entries(this.#config.matchers.lists)) {
- for (let fieldName of matcherNames) {
+ for (const [listName, matcherNames] of Object.entries(this.#config.matchers.lists)) {
+ for (const fieldName of matcherNames) {
if (!this.#matcherLists[listName]) {
this.#matcherLists[listName] = [];
}
@@ -13178,7 +13377,7 @@ class Matching {
* @type {string[]}
*/
const selectors = [];
- for (let matcher of matcherList) {
+ for (const matcher of matcherList) {
if (matcher.strategies.cssSelector) {
const css = this.cssSelector(matcher.strategies.cssSelector);
if (css) {
@@ -13315,12 +13514,12 @@ class Matching {
/**
* Loop through each strategy in order
*/
- for (let strategyName of this.#defaultStrategyOrder) {
+ for (const strategyName of this.#defaultStrategyOrder) {
let result;
/**
* Now loop through each matcher in the list.
*/
- for (let matcher of matchers) {
+ for (const matcher of matchers) {
/**
* for each `strategyName` (such as cssSelector), check
* if the current matcher implements it.
@@ -13445,16 +13644,16 @@ class Matching {
if (!ddgMatcher || !ddgMatcher.match) {
return defaultResult;
}
- let matchRexExp = this.getDDGMatcherRegex(lookup);
+ const matchRexExp = this.getDDGMatcherRegex(lookup);
if (!matchRexExp) {
return defaultResult;
}
- let requiredScore = ['match', 'forceUnknown', 'maxDigits'].filter(ddgMatcherProp => ddgMatcherProp in ddgMatcher).length;
+ const requiredScore = ['match', 'forceUnknown', 'maxDigits'].filter(ddgMatcherProp => ddgMatcherProp in ddgMatcher).length;
/** @type {MatchableStrings[]} */
const matchableStrings = ddgMatcher.matchableStrings || ['labelText', 'placeholderAttr', 'relatedText'];
- for (let stringName of matchableStrings) {
- let elementString = this.activeElementStrings[stringName];
+ for (const stringName of matchableStrings) {
+ const elementString = this.activeElementStrings[stringName];
if (!elementString) continue;
// Scoring to ensure all DDG tests are valid
@@ -13470,7 +13669,7 @@ class Matching {
// If a negated regex was provided, ensure it does not match
// If it DOES match - then we need to prevent any future strategies from continuing
if (ddgMatcher.forceUnknown) {
- let notRegex = ddgMatcher.forceUnknown;
+ const notRegex = ddgMatcher.forceUnknown;
if (!notRegex) {
return {
...result,
@@ -13489,7 +13688,7 @@ class Matching {
}
}
if (ddgMatcher.skip) {
- let skipRegex = ddgMatcher.skip;
+ const skipRegex = ddgMatcher.skip;
if (!skipRegex) {
return {
...result,
@@ -13554,8 +13753,8 @@ class Matching {
}
/** @type {MatchableStrings[]} */
const stringsToMatch = ['placeholderAttr', 'nameAttr', 'labelText', 'id', 'relatedText'];
- for (let stringName of stringsToMatch) {
- let elementString = this.activeElementStrings[stringName];
+ for (const stringName of stringsToMatch) {
+ const elementString = this.activeElementStrings[stringName];
if (!elementString) continue;
if ((0, _autofillUtils.safeRegexTest)(regex, elementString)) {
return {
@@ -13629,14 +13828,14 @@ class Matching {
fields: {}
},
strategies: {
- 'vendorRegex': {
+ vendorRegex: {
rules: {},
ruleSets: []
},
- 'ddgMatcher': {
+ ddgMatcher: {
matchers: {}
},
- 'cssSelector': {
+ cssSelector: {
selectors: {}
}
}
@@ -13807,7 +14006,7 @@ const removeExcessWhitespace = function () {
exports.removeExcessWhitespace = removeExcessWhitespace;
const getExplicitLabelsText = el => {
const labelTextCandidates = [];
- for (let label of el.labels || []) {
+ for (const label of el.labels || []) {
labelTextCandidates.push(...(0, _labelUtil.extractElementStrings)(label));
}
if (el.hasAttribute('aria-label')) {
@@ -13862,7 +14061,7 @@ const getRelatedText = (el, form, cssSelector) => {
// If we didn't find a container, try looking for an adjacent label
if (scope === el) {
- let previousEl = recursiveGetPreviousElSibling(el);
+ const previousEl = recursiveGetPreviousElSibling(el);
if (previousEl instanceof HTMLElement) {
scope = previousEl;
}
@@ -14521,19 +14720,18 @@ class DefaultScanner {
if (this.device.globalConfig.isDDGDomain) {
return this;
}
- if ('matches' in context && context.matches?.(this.matching.cssSelector('formInputsSelectorWithoutSelect'))) {
+ const formInputsSelectorWithoutSelect = this.matching.cssSelector('formInputsSelectorWithoutSelect');
+ if ('matches' in context && context.matches?.(formInputsSelectorWithoutSelect)) {
this.addInput(context);
} else {
- const selector = this.matching.cssSelector('formInputsSelectorWithoutSelect');
- const inputs = context.querySelectorAll(selector);
+ const inputs = context.querySelectorAll(formInputsSelectorWithoutSelect);
if (inputs.length > this.options.maxInputsPerPage) {
this.setMode('stopped', `Too many input fields in the given context (${inputs.length}), stop scanning`, context);
return this;
}
inputs.forEach(input => this.addInput(input));
if (context instanceof HTMLFormElement && this.forms.get(context)?.hasShadowTree) {
- const selector = this.matching.cssSelector('formInputsSelectorWithoutSelect');
- (0, _autofillUtils.findEnclosedElements)(context, selector).forEach(input => {
+ (0, _autofillUtils.findElementsInShadowTree)(context, formInputsSelectorWithoutSelect).forEach(input => {
if (input instanceof HTMLInputElement) {
this.addInput(input, context);
}
@@ -14619,12 +14817,16 @@ class DefaultScanner {
}
if (element.parentElement) {
element = element.parentElement;
- const inputs = element.querySelectorAll(this.matching.cssSelector('formInputsSelector'));
- const buttons = element.querySelectorAll(this.matching.cssSelector('submitButtonSelector'));
- // If we find a button or another input, we assume that's our form
- if (inputs.length > 1 || buttons.length) {
- // found related input, return common ancestor
- return element;
+ // If the parent is a redundant component (only contains a single element or is a shadowRoot) do not increase the traversal count.
+ if (element.childElementCount > 1) {
+ const inputs = element.querySelectorAll(this.matching.cssSelector('formInputsSelector'));
+ const buttons = element.querySelectorAll(this.matching.cssSelector('submitButtonSelector'));
+ // If we find a button or another input, we assume that's our form
+ if (inputs.length > 1 || buttons.length) {
+ // found related input, return common ancestor
+ return element;
+ }
+ traversalLayerCount++;
}
} else {
// possibly a shadow boundary, so traverse through the shadow root and find the form
@@ -14632,9 +14834,11 @@ class DefaultScanner {
if (root instanceof ShadowRoot && root.host) {
// @ts-ignore
element = root.host;
+ } else {
+ // We're in a strange state (no parent or shadow root), just break out of the loop for safety
+ break;
}
}
- traversalLayerCount++;
}
return input;
}
@@ -14717,7 +14921,7 @@ class DefaultScanner {
this.changedElements.clear();
} else if (!this.rescanAll) {
// otherwise keep adding each element to the queue
- for (let element of htmlElements) {
+ for (const element of htmlElements) {
this.changedElements.add(element);
}
}
@@ -14741,7 +14945,7 @@ class DefaultScanner {
this.findEligibleInputs(document);
return;
}
- for (let element of this.changedElements) {
+ for (const element of this.changedElements) {
if (element.isConnected) {
this.findEligibleInputs(element);
}
@@ -14762,7 +14966,7 @@ class DefaultScanner {
const outgoing = [];
for (const mutationRecord of mutationList) {
if (mutationRecord.type === 'childList') {
- for (let addedNode of mutationRecord.addedNodes) {
+ for (const addedNode of mutationRecord.addedNodes) {
if (!(addedNode instanceof HTMLElement)) continue;
if (addedNode.nodeName === 'DDG-AUTOFILL') continue;
outgoing.push(addedNode);
@@ -14796,12 +15000,13 @@ class DefaultScanner {
// find the enclosing parent form, and scan it.
if (realTarget instanceof HTMLInputElement && !realTarget.hasAttribute(ATTR_INPUT_TYPE)) {
const parentForm = this.getParentForm(realTarget);
- if (parentForm && parentForm instanceof HTMLFormElement) {
- const hasShadowTree = event.target?.shadowRoot != null;
- const form = new _Form.Form(parentForm, realTarget, this.device, this.matching, this.shouldAutoprompt, hasShadowTree);
- this.forms.set(parentForm, form);
- this.findEligibleInputs(parentForm);
- }
+
+ // If the parent form is an input element we bail.
+ if (parentForm instanceof HTMLInputElement) return;
+ const hasShadowTree = event.target?.shadowRoot != null;
+ const form = new _Form.Form(parentForm, realTarget, this.device, this.matching, this.shouldAutoprompt, hasShadowTree);
+ this.forms.set(parentForm, form);
+ this.findEligibleInputs(parentForm);
}
window.performance?.mark?.('scan_shadow:init:end');
(0, _autofillUtils.logPerformance)('scan_shadow');
@@ -15176,7 +15381,8 @@ class Settings {
inputType_credentials: false,
inputType_creditCards: false,
inlineIcon_credentials: false,
- unknown_username_categorization: false
+ unknown_username_categorization: false,
+ partial_form_saves: false
},
/** @type {AvailableInputTypes} */
availableInputTypes: {
@@ -15500,7 +15706,7 @@ ${css}
if (btn.matches('.wrapper:not(.top-autofill) button:hover, .currentFocus')) {
callbacks.onSelect(btn.id);
} else {
- console.warn('The button doesn\'t seem to be hovered. Please check.');
+ console.warn("The button doesn't seem to be hovered. Please check.");
}
});
});
@@ -15704,7 +15910,9 @@ const defaultOptions = exports.defaultOptions = {
}`,
css: ``,
setSize: undefined,
- remove: () => {/** noop */},
+ remove: () => {
+ /** noop */
+ },
testMode: false,
checkVisibility: true,
hasCaret: false,
@@ -15732,9 +15940,9 @@ class HTMLTooltip {
this.tooltip = null;
this.getPosition = getPosition;
const forcedVisibilityStyles = {
- 'display': 'block',
- 'visibility': 'visible',
- 'opacity': '1'
+ display: 'block',
+ visibility: 'visible',
+ opacity: '1'
};
// @ts-ignore how to narrow this.host to HTMLElement?
(0, _autofillUtils.addInlineStyles)(this.host, forcedVisibilityStyles);
@@ -15992,7 +16200,7 @@ class HTMLTooltip {
checkVisibility: this.options.checkVisibility
});
} else {
- console.warn('The button doesn\'t seem to be hovered. Please check.');
+ console.warn("The button doesn't seem to be hovered. Please check.");
}
}
}
@@ -16374,25 +16582,27 @@ class HTMLTooltipUIController extends _UIController.UIController {
/**
* Called when clicking on the Manage… button in the html tooltip
- *
* @param {SupportedMainTypes} type
* @returns {*}
* @private
*/
_onManage(type) {
- this.removeTooltip();
switch (type) {
case 'credentials':
- return this._options.device.openManagePasswords();
+ this._options.device.openManagePasswords();
+ break;
case 'creditCards':
- return this._options.device.openManageCreditCards();
+ this._options.device.openManageCreditCards();
+ break;
case 'identities':
- return this._options.device.openManageIdentities();
+ this._options.device.openManageIdentities();
+ break;
default:
// noop
}
- }
+ this.removeTooltip();
+ }
_onIncontextSignupDismissed(_ref) {
let {
hasOtherOptions
@@ -16936,10 +17146,14 @@ Object.defineProperty(exports, "__esModule", {
});
exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0;
exports.escapeXML = escapeXML;
-exports.findEnclosedElements = findEnclosedElements;
+exports.findElementsInShadowTree = findElementsInShadowTree;
exports.formatDuckAddress = void 0;
exports.getActiveElement = getActiveElement;
-exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = exports.getDaxBoundingBox = void 0;
+exports.getDaxBoundingBox = void 0;
+exports.getFormControlElements = getFormControlElements;
+exports.getTextShallow = void 0;
+exports.hasUsernameLikeIdentity = hasUsernameLikeIdentity;
+exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = void 0;
exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper;
exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0;
exports.isLocalNetwork = isLocalNetwork;
@@ -16948,6 +17162,7 @@ exports.isValidTLD = isValidTLD;
exports.logPerformance = logPerformance;
exports.notifyWebApp = void 0;
exports.pierceShadowTree = pierceShadowTree;
+exports.queryElementsWithShadow = queryElementsWithShadow;
exports.safeExecute = exports.removeInlineStyles = void 0;
exports.safeRegexTest = safeRegexTest;
exports.setValue = exports.sendAndWaitForAnswer = void 0;
@@ -17320,8 +17535,9 @@ const isLikelyASubmitButton = (el, matching) => {
// has high-signal submit classes
safeRegexTest(/submit/i, dataTestId) || safeRegexTest(matching.getDDGMatcherRegex('submitButtonRegex'), text) ||
// has high-signal text
- el.offsetHeight * el.offsetWidth >= 10000 && !safeRegexTest(/secondary/i, el.className) // it's a large element 250x40px
- ) && el.offsetHeight * el.offsetWidth >= 2000 &&
+ el.offsetHeight * el.offsetWidth >= 10000 && !safeRegexTest(/secondary/i, el.className)) &&
+ // it's a large element 250x40px
+ el.offsetHeight * el.offsetWidth >= 2000 &&
// it's not a very small button like inline links and such
!safeRegexTest(matching.getDDGMatcherRegex('submitButtonUnlikelyRegex'), text + ' ' + ariaLabel);
};
@@ -17549,22 +17765,16 @@ function getActiveElement() {
}
/**
- * Takes a root element and tries to find visible elements first, and if it fails, it tries to find shadow elements
+ * Takes a root element and tries to find elements in shadow DOMs that match the selector
* @param {HTMLElement|HTMLFormElement} root
* @param {string} selector
* @returns {Element[]}
*/
-function findEnclosedElements(root, selector) {
- // Check if there are any normal elements that match the selector
- const elements = root.querySelectorAll(selector);
- if (elements.length > 0) {
- return Array.from(elements);
- }
-
- // Check if there are any shadow elements that match the selector
+function findElementsInShadowTree(root, selector) {
const shadowElements = [];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
- let node = walker.nextNode();
+ /** @type {Node|null} */
+ let node = walker.currentNode;
while (node) {
if (node instanceof HTMLElement && node.shadowRoot) {
shadowElements.push(...node.shadowRoot.querySelectorAll(selector));
@@ -17574,6 +17784,52 @@ function findEnclosedElements(root, selector) {
return shadowElements;
}
+/**
+ * The function looks for form's control elements, and returns them if they're iterable.
+ * @param {HTMLElement} form
+ * @param {string} selector
+ * @returns {Element[]|null}
+ */
+function getFormControlElements(form, selector) {
+ // Some sites seem to be overriding `form.elements`, so we need to check if it's still iterable.
+ if (form instanceof HTMLFormElement && form.elements != null && Symbol.iterator in Object(form.elements)) {
+ // For form elements we use .elements to catch fields outside the form itself using the form attribute.
+ // It also catches all elements when the markup is broken.
+ // We use .filter to avoid specific types of elements.
+ const formControls = [...form.elements].filter(el => el.matches(selector));
+ return [...formControls];
+ } else {
+ return null;
+ }
+}
+
+/**
+ * Default operation: finds elements using querySelectorAll.
+ * Optionally, can be forced to scan the shadow tree.
+ * @param {HTMLElement} element
+ * @param {string} selector
+ * @param {boolean} forceScanShadowTree
+ * @returns {Element[]}
+ */
+function queryElementsWithShadow(element, selector) {
+ let forceScanShadowTree = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+ /** @type {Element[]|NodeListOf} element */
+ const elements = element.querySelectorAll(selector);
+ if (forceScanShadowTree || elements.length === 0) {
+ return [...elements, ...findElementsInShadowTree(element, selector)];
+ }
+ return [...elements];
+}
+
+/**
+ * Checks if there is a single username-like identity, i.e. email or phone
+ * @param {InternalIdentityObject} identities
+ * @returns {boolean}
+ */
+function hasUsernameLikeIdentity(identities) {
+ return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone);
+}
+
},{"./Form/matching.js":44,"./constants.js":67,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],65:[function(require,module,exports){
"use strict";
@@ -17613,7 +17869,8 @@ Object.defineProperty(exports, "__esModule", {
});
exports.DDG_DOMAIN_REGEX = void 0;
exports.createGlobalConfig = createGlobalConfig;
-const DDG_DOMAIN_REGEX = exports.DDG_DOMAIN_REGEX = new RegExp(/^https:\/\/(([a-z0-9-_]+?)\.)?duckduckgo\.com\/email/);
+/* eslint-disable prefer-const */
+const DDG_DOMAIN_REGEX = exports.DDG_DOMAIN_REGEX = /^https:\/\/(([a-z0-9-_]+?)\.)?duckduckgo\.com\/email/;
/**
* This is a centralised place to contain all string/variable replacements
@@ -18124,6 +18381,26 @@ const availableInputTypesSchema = exports.availableInputTypesSchema = _zod.z.obj
credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).optional(),
credentialsImport: _zod.z.boolean().optional()
});
+const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = _zod.z.object({
+ type: _zod.z.literal("getAutofillInitDataResponse").optional(),
+ success: _zod.z.object({
+ credentials: _zod.z.array(credentialsSchema),
+ identities: _zod.z.array(_zod.z.record(_zod.z.unknown())),
+ creditCards: _zod.z.array(_zod.z.record(_zod.z.unknown())),
+ serializedInputContext: _zod.z.string()
+ }).optional(),
+ error: genericErrorSchema.optional()
+});
+const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultSchema = _zod.z.object({
+ type: _zod.z.literal("getAutofillCredentialsResponse").optional(),
+ success: _zod.z.object({
+ id: _zod.z.string().optional(),
+ autogenerated: _zod.z.boolean().optional(),
+ username: _zod.z.string(),
+ password: _zod.z.string().optional()
+ }).optional(),
+ error: genericErrorSchema.optional()
+});
const availableInputTypes1Schema = exports.availableInputTypes1Schema = _zod.z.object({
credentials: _zod.z.object({
username: _zod.z.boolean().optional(),
@@ -18156,6 +18433,11 @@ const availableInputTypes1Schema = exports.availableInputTypes1Schema = _zod.z.o
credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).optional(),
credentialsImport: _zod.z.boolean().optional()
});
+const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = _zod.z.object({
+ status: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]),
+ credentials: _zod.z.array(credentialsSchema),
+ availableInputTypes: availableInputTypes1Schema
+});
const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = _zod.z.object({
inputType_credentials: _zod.z.boolean().optional(),
inputType_identities: _zod.z.boolean().optional(),
@@ -18166,56 +18448,8 @@ const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = _zod
credentials_saving: _zod.z.boolean().optional(),
inlineIcon_credentials: _zod.z.boolean().optional(),
third_party_credentials_provider: _zod.z.boolean().optional(),
- unknown_username_categorization: _zod.z.boolean().optional()
-});
-const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = _zod.z.object({
- generatedPassword: generatedPasswordSchema.optional(),
- inputType: _zod.z.string(),
- mainType: _zod.z.union([_zod.z.literal("credentials"), _zod.z.literal("identities"), _zod.z.literal("creditCards")]),
- subType: _zod.z.string(),
- trigger: _zod.z.union([_zod.z.literal("userInitiated"), _zod.z.literal("autoprompt"), _zod.z.literal("postSignup")]).optional(),
- serializedInputContext: _zod.z.string().optional(),
- triggerContext: triggerContextSchema.optional()
-});
-const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = _zod.z.object({
- type: _zod.z.literal("getAutofillDataResponse").optional(),
- success: _zod.z.object({
- credentials: credentialsSchema.optional(),
- action: _zod.z.union([_zod.z.literal("fill"), _zod.z.literal("focus"), _zod.z.literal("none"), _zod.z.literal("refreshAvailableInputTypes"), _zod.z.literal("acceptGeneratedPassword"), _zod.z.literal("rejectGeneratedPassword")])
- }).optional(),
- error: genericErrorSchema.optional()
-});
-const storeFormDataSchema = exports.storeFormDataSchema = _zod.z.object({
- credentials: outgoingCredentialsSchema.optional(),
- trigger: _zod.z.union([_zod.z.literal("formSubmission"), _zod.z.literal("passwordGeneration"), _zod.z.literal("emailProtection")]).optional()
-});
-const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = _zod.z.object({
- type: _zod.z.literal("getAvailableInputTypesResponse").optional(),
- success: availableInputTypesSchema,
- error: genericErrorSchema.optional()
-});
-const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = _zod.z.object({
- type: _zod.z.literal("getAutofillInitDataResponse").optional(),
- success: _zod.z.object({
- credentials: _zod.z.array(credentialsSchema),
- identities: _zod.z.array(_zod.z.record(_zod.z.unknown())),
- creditCards: _zod.z.array(_zod.z.record(_zod.z.unknown())),
- serializedInputContext: _zod.z.string()
- }).optional(),
- error: genericErrorSchema.optional()
-});
-const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultSchema = _zod.z.object({
- type: _zod.z.literal("getAutofillCredentialsResponse").optional(),
- success: _zod.z.object({
- id: _zod.z.string().optional(),
- autogenerated: _zod.z.boolean().optional(),
- username: _zod.z.string(),
- password: _zod.z.string().optional()
- }).optional(),
- error: genericErrorSchema.optional()
-});
-const autofillSettingsSchema = exports.autofillSettingsSchema = _zod.z.object({
- featureToggles: autofillFeatureTogglesSchema
+ unknown_username_categorization: _zod.z.boolean().optional(),
+ partial_form_saves: _zod.z.boolean().optional()
});
const emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = _zod.z.object({
success: _zod.z.boolean().optional(),
@@ -18251,19 +18485,30 @@ const emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtection
}).optional(),
error: genericErrorSchema.optional()
});
-const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = _zod.z.object({
- contentScope: contentScopeSchema,
- userUnprotectedDomains: _zod.z.array(_zod.z.string()),
- userPreferences: userPreferencesSchema
+const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = _zod.z.object({
+ generatedPassword: generatedPasswordSchema.optional(),
+ inputType: _zod.z.string(),
+ mainType: _zod.z.union([_zod.z.literal("credentials"), _zod.z.literal("identities"), _zod.z.literal("creditCards")]),
+ subType: _zod.z.string(),
+ trigger: _zod.z.union([_zod.z.literal("userInitiated"), _zod.z.literal("autoprompt"), _zod.z.literal("postSignup")]).optional(),
+ serializedInputContext: _zod.z.string().optional(),
+ triggerContext: triggerContextSchema.optional()
});
-const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = _zod.z.object({
- status: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]),
- credentials: _zod.z.array(credentialsSchema),
- availableInputTypes: availableInputTypes1Schema
+const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = _zod.z.object({
+ type: _zod.z.literal("getAutofillDataResponse").optional(),
+ success: _zod.z.object({
+ credentials: credentialsSchema.optional(),
+ action: _zod.z.union([_zod.z.literal("fill"), _zod.z.literal("focus"), _zod.z.literal("none"), _zod.z.literal("refreshAvailableInputTypes"), _zod.z.literal("acceptGeneratedPassword"), _zod.z.literal("rejectGeneratedPassword")])
+ }).optional(),
+ error: genericErrorSchema.optional()
});
-const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = _zod.z.object({
- type: _zod.z.literal("getRuntimeConfigurationResponse").optional(),
- success: runtimeConfigurationSchema.optional(),
+const storeFormDataSchema = exports.storeFormDataSchema = _zod.z.object({
+ credentials: outgoingCredentialsSchema.optional(),
+ trigger: _zod.z.union([_zod.z.literal("partialSave"), _zod.z.literal("formSubmission"), _zod.z.literal("passwordGeneration"), _zod.z.literal("emailProtection")]).optional()
+});
+const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = _zod.z.object({
+ type: _zod.z.literal("getAvailableInputTypesResponse").optional(),
+ success: availableInputTypesSchema,
error: genericErrorSchema.optional()
});
const askToUnlockProviderResultSchema = exports.askToUnlockProviderResultSchema = _zod.z.object({
@@ -18276,6 +18521,19 @@ const checkCredentialsProviderStatusResultSchema = exports.checkCredentialsProvi
success: providerStatusUpdatedSchema,
error: genericErrorSchema.optional()
});
+const autofillSettingsSchema = exports.autofillSettingsSchema = _zod.z.object({
+ featureToggles: autofillFeatureTogglesSchema
+});
+const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = _zod.z.object({
+ contentScope: contentScopeSchema,
+ userUnprotectedDomains: _zod.z.array(_zod.z.string()),
+ userPreferences: userPreferencesSchema
+});
+const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = _zod.z.object({
+ type: _zod.z.literal("getRuntimeConfigurationResponse").optional(),
+ success: runtimeConfigurationSchema.optional(),
+ error: genericErrorSchema.optional()
+});
const apiSchema = exports.apiSchema = _zod.z.object({
addDebugFlag: _zod.z.record(_zod.z.unknown()).and(_zod.z.object({
paramsValidator: addDebugFlagParamsSchema.optional()
@@ -18492,7 +18750,7 @@ function waitForResponse(expectedResponse, config) {
return;
}
try {
- let data = JSON.parse(e.data);
+ const data = JSON.parse(e.data);
if (data.type === expectedResponse) {
window.removeEventListener('message', handler);
return resolve(data);
@@ -18647,7 +18905,7 @@ async function extensionSpecificRuntimeConfiguration(deviceApi) {
return {
success: {
// @ts-ignore
- contentScope: contentScope,
+ contentScope,
// @ts-ignore
userPreferences: {
// Copy locale to user preferences as 'language' to match expected payload
@@ -18841,6 +19099,7 @@ function waitForWindowsResponse(responseId, options) {
if (options?.signal?.aborted) {
return reject(new DOMException('Aborted', 'AbortError'));
}
+ // eslint-disable-next-line prefer-const
let teardown;
// The event handler
@@ -21727,7 +21986,6 @@ exports.default = void 0;
window.requestIdleCallback = window.requestIdleCallback || function (cb) {
return setTimeout(function () {
const start = Date.now();
- // eslint-disable-next-line standard/no-callback-literal
cb({
didTimeout: false,
timeRemaining: function () {
diff --git a/node_modules/@duckduckgo/autofill/dist/autofill.js b/node_modules/@duckduckgo/autofill/dist/autofill.js
index 6a97e06c86fa..c4584483dadf 100644
--- a/node_modules/@duckduckgo/autofill/dist/autofill.js
+++ b/node_modules/@duckduckgo/autofill/dist/autofill.js
@@ -269,8 +269,8 @@ class SchemaValidationError extends Error {
}
case 'invalid_union':
{
- for (let unionError of issue.unionErrors) {
- for (let issue1 of unionError.issues) {
+ for (const unionError of issue.unionErrors) {
+ for (const issue1 of unionError.issues) {
log(issue1);
}
}
@@ -282,7 +282,7 @@ class SchemaValidationError extends Error {
}
}
}
- for (let error of errors) {
+ for (const error of errors) {
log(error);
}
const message = [heading, 'please see the details above'].join('\n ');
@@ -405,8 +405,8 @@ class DeviceApi {
*/
async request(deviceApiCall, options) {
deviceApiCall.validateParams();
- let result = await this.transport.send(deviceApiCall, options);
- let processed = deviceApiCall.preResultValidation(result);
+ const result = await this.transport.send(deviceApiCall, options);
+ const processed = deviceApiCall.preResultValidation(result);
return deviceApiCall.validateResult(processed);
}
/**
@@ -494,44 +494,44 @@ var _webkit = require("./webkit.js");
*/
class Messaging {
/**
- * @param {WebkitMessagingConfig} config
- */
+ * @param {WebkitMessagingConfig} config
+ */
constructor(config) {
this.transport = getTransport(config);
}
/**
- * Send a 'fire-and-forget' message.
- * @throws {Error}
- * {@link MissingHandler}
- *
- * @example
- *
- * ```
- * const messaging = new Messaging(config)
- * messaging.notify("foo", {bar: "baz"})
- * ```
- * @param {string} name
- * @param {Record} [data]
- */
+ * Send a 'fire-and-forget' message.
+ * @throws {Error}
+ * {@link MissingHandler}
+ *
+ * @example
+ *
+ * ```
+ * const messaging = new Messaging(config)
+ * messaging.notify("foo", {bar: "baz"})
+ * ```
+ * @param {string} name
+ * @param {Record} [data]
+ */
notify(name) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
this.transport.notify(name, data);
}
/**
- * Send a request, and wait for a response
- * @throws {Error}
- * {@link MissingHandler}
- *
- * @example
- * ```
- * const messaging = new Messaging(config)
- * const response = await messaging.request("foo", {bar: "baz"})
- * ```
- *
- * @param {string} name
- * @param {Record} [data]
- * @return {Promise}
- */
+ * Send a request, and wait for a response
+ * @throws {Error}
+ * {@link MissingHandler}
+ *
+ * @example
+ * ```
+ * const messaging = new Messaging(config)
+ * const response = await messaging.request("foo", {bar: "baz"})
+ * ```
+ *
+ * @param {string} name
+ * @param {Record} [data]
+ * @return {Promise}
+ */
request(name) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
return this.transport.request(name, data);
@@ -544,20 +544,20 @@ class Messaging {
exports.Messaging = Messaging;
class MessagingTransport {
/**
- * @param {string} name
- * @param {Record} [data]
- * @returns {void}
- */
+ * @param {string} name
+ * @param {Record} [data]
+ * @returns {void}
+ */
// @ts-ignore - ignoring a no-unused ts error, this is only an interface.
notify(name) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
throw new Error("must implement 'notify'");
}
/**
- * @param {string} name
- * @param {Record} [data]
- * @return {Promise}
- */
+ * @param {string} name
+ * @param {Record} [data]
+ * @return {Promise}
+ */
// @ts-ignore - ignoring a no-unused ts error, this is only an interface.
request(name) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -582,9 +582,9 @@ function getTransport(config) {
*/
class MissingHandler extends Error {
/**
- * @param {string} message
- * @param {string} handlerName
- */
+ * @param {string} message
+ * @param {string} handlerName
+ */
constructor(message, handlerName) {
super(message);
this.handlerName = handlerName;
@@ -690,8 +690,8 @@ class WebkitMessagingTransport {
config;
globals;
/**
- * @param {WebkitMessagingConfig} config
- */
+ * @param {WebkitMessagingConfig} config
+ */
constructor(config) {
this.config = config;
this.globals = captureGlobals();
@@ -700,11 +700,11 @@ class WebkitMessagingTransport {
}
}
/**
- * Sends message to the webkit layer (fire and forget)
- * @param {String} handler
- * @param {*} data
- * @internal
- */
+ * Sends message to the webkit layer (fire and forget)
+ * @param {String} handler
+ * @param {*} data
+ * @internal
+ */
wkSend(handler) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
if (!(handler in this.globals.window.webkit.messageHandlers)) {
@@ -728,12 +728,12 @@ class WebkitMessagingTransport {
}
/**
- * Sends message to the webkit layer and waits for the specified response
- * @param {String} handler
- * @param {*} data
- * @returns {Promise<*>}
- * @internal
- */
+ * Sends message to the webkit layer and waits for the specified response
+ * @param {String} handler
+ * @param {*} data
+ * @returns {Promise<*>}
+ * @internal
+ */
async wkSendAndWait(handler) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
if (this.config.hasModernWebkitAPI) {
@@ -774,27 +774,27 @@ class WebkitMessagingTransport {
}
}
/**
- * @param {string} name
- * @param {Record} [data]
- */
+ * @param {string} name
+ * @param {Record} [data]
+ */
notify(name) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
this.wkSend(name, data);
}
/**
- * @param {string} name
- * @param {Record} [data]
- */
+ * @param {string} name
+ * @param {Record} [data]
+ */
request(name) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
return this.wkSendAndWait(name, data);
}
/**
- * Generate a random method name and adds it to the global scope
- * The native layer will use this method to send the response
- * @param {string | number} randomMethodName
- * @param {Function} callback
- */
+ * Generate a random method name and adds it to the global scope
+ * The native layer will use this method to send the response
+ * @param {string | number} randomMethodName
+ * @param {Function} callback
+ */
generateRandomMethod(randomMethodName, callback) {
var _this = this;
this.globals.ObjectDefineProperty(this.globals.window, randomMethodName, {
@@ -803,8 +803,8 @@ class WebkitMessagingTransport {
configurable: true,
writable: false,
/**
- * @param {any[]} args
- */
+ * @param {any[]} args
+ */
value: function () {
callback(...arguments);
// @ts-ignore - we want this to throw if it fails as it would indicate a fatal error.
@@ -820,16 +820,16 @@ class WebkitMessagingTransport {
}
/**
- * @type {{name: string, length: number}}
- */
+ * @type {{name: string, length: number}}
+ */
algoObj = {
name: 'AES-GCM',
length: 256
};
/**
- * @returns {Promise}
- */
+ * @returns {Promise}
+ */
async createRandKey() {
const key = await this.globals.generateKey(this.algoObj, true, ['encrypt', 'decrypt']);
const exportedKey = await this.globals.exportKey('raw', key);
@@ -837,44 +837,44 @@ class WebkitMessagingTransport {
}
/**
- * @returns {Uint8Array}
- */
+ * @returns {Uint8Array}
+ */
createRandIv() {
return this.globals.getRandomValues(new this.globals.Uint8Array(12));
}
/**
- * @param {BufferSource} ciphertext
- * @param {BufferSource} key
- * @param {Uint8Array} iv
- * @returns {Promise}
- */
+ * @param {BufferSource} ciphertext
+ * @param {BufferSource} key
+ * @param {Uint8Array} iv
+ * @returns {Promise}
+ */
async decrypt(ciphertext, key, iv) {
const cryptoKey = await this.globals.importKey('raw', key, 'AES-GCM', false, ['decrypt']);
const algo = {
name: 'AES-GCM',
iv
};
- let decrypted = await this.globals.decrypt(algo, cryptoKey, ciphertext);
- let dec = new this.globals.TextDecoder();
+ const decrypted = await this.globals.decrypt(algo, cryptoKey, ciphertext);
+ const dec = new this.globals.TextDecoder();
return dec.decode(decrypted);
}
/**
- * When required (such as on macos 10.x), capture the `postMessage` method on
- * each webkit messageHandler
- *
- * @param {string[]} handlerNames
- */
+ * When required (such as on macos 10.x), capture the `postMessage` method on
+ * each webkit messageHandler
+ *
+ * @param {string[]} handlerNames
+ */
captureWebkitHandlers(handlerNames) {
const handlers = window.webkit.messageHandlers;
if (!handlers) throw new _messaging.MissingHandler('window.webkit.messageHandlers was absent', 'all');
- for (let webkitMessageHandlerName of handlerNames) {
+ for (const webkitMessageHandlerName of handlerNames) {
if (typeof handlers[webkitMessageHandlerName]?.postMessage === 'function') {
/**
- * `bind` is used here to ensure future calls to the captured
- * `postMessage` have the correct `this` context
- */
+ * `bind` is used here to ensure future calls to the captured
+ * `postMessage` have the correct `this` context
+ */
const original = handlers[webkitMessageHandlerName];
const bound = handlers[webkitMessageHandlerName].postMessage?.bind(original);
this.globals.capturedWebkitHandlers[webkitMessageHandlerName] = bound;
@@ -903,11 +903,11 @@ class WebkitMessagingTransport {
exports.WebkitMessagingTransport = WebkitMessagingTransport;
class WebkitMessagingConfig {
/**
- * @param {object} params
- * @param {boolean} params.hasModernWebkitAPI
- * @param {string[]} params.webkitMessageHandlerNames
- * @param {string} params.secret
- */
+ * @param {object} params
+ * @param {boolean} params.hasModernWebkitAPI
+ * @param {string[]} params.webkitMessageHandlerNames
+ * @param {string} params.secret
+ */
constructor(params) {
/**
* Whether or not the current WebKit Platform supports secure messaging
@@ -915,13 +915,13 @@ class WebkitMessagingConfig {
*/
this.hasModernWebkitAPI = params.hasModernWebkitAPI;
/**
- * A list of WebKit message handler names that a user script can send
- */
+ * A list of WebKit message handler names that a user script can send
+ */
this.webkitMessageHandlerNames = params.webkitMessageHandlerNames;
/**
- * A string provided by native platforms to be sent with future outgoing
- * messages
- */
+ * A string provided by native platforms to be sent with future outgoing
+ * messages
+ */
this.secret = params.secret;
}
}
@@ -933,28 +933,28 @@ class WebkitMessagingConfig {
exports.WebkitMessagingConfig = WebkitMessagingConfig;
class SecureMessagingParams {
/**
- * @param {object} params
- * @param {string} params.methodName
- * @param {string} params.secret
- * @param {number[]} params.key
- * @param {number[]} params.iv
- */
+ * @param {object} params
+ * @param {string} params.methodName
+ * @param {string} params.secret
+ * @param {number[]} params.key
+ * @param {number[]} params.iv
+ */
constructor(params) {
/**
* The method that's been appended to `window` to be called later
*/
this.methodName = params.methodName;
/**
- * The secret used to ensure message sender validity
- */
+ * The secret used to ensure message sender validity
+ */
this.secret = params.secret;
/**
- * The CipherKey as number[]
- */
+ * The CipherKey as number[]
+ */
this.key = params.key;
/**
- * The Initial Vector as number[]
- */
+ * The Initial Vector as number[]
+ */
this.iv = params.iv;
}
}
@@ -1165,7 +1165,7 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj &&
* }} PasswordParameters
*/
const defaults = Object.freeze({
- SCAN_SET_ORDER: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-~!@#$%^&*_+=`|(){}[:;\\\"'<>,.?/ ]",
+ SCAN_SET_ORDER: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-~!@#$%^&*_+=`|(){}[:;\\"\'<>,.?/ ]',
defaultUnambiguousCharacters: 'abcdefghijkmnopqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ0123456789',
defaultPasswordLength: _constants.constants.DEFAULT_MIN_LENGTH,
defaultPasswordRules: _constants.constants.DEFAULT_PASSWORD_RULES,
@@ -1292,7 +1292,7 @@ class Password {
_requirementsFromRules(passwordRules) {
/** @type {Requirements} */
const requirements = {};
- for (let rule of passwordRules) {
+ for (const rule of passwordRules) {
if (rule.name === parser.RuleName.ALLOWED) {
console.assert(!('PasswordAllowedCharacters' in requirements));
const chars = this._charactersFromCharactersClasses(rule.value);
@@ -1623,7 +1623,7 @@ class Password {
*/
_charactersFromCharactersClasses(characterClasses) {
const output = [];
- for (let characterClass of characterClasses) {
+ for (const characterClass of characterClasses) {
output.push(...this._scanSetFromCharacterClass(characterClass));
}
return output;
@@ -1637,9 +1637,9 @@ class Password {
if (!characters.length) {
return '';
}
- let shadowCharacters = Array.prototype.slice.call(characters);
+ const shadowCharacters = Array.prototype.slice.call(characters);
shadowCharacters.sort((a, b) => this.options.SCAN_SET_ORDER.indexOf(a) - this.options.SCAN_SET_ORDER.indexOf(b));
- let uniqueCharacters = [shadowCharacters[0]];
+ const uniqueCharacters = [shadowCharacters[0]];
for (let i = 1, length = shadowCharacters.length; i < length; ++i) {
if (shadowCharacters[i] === shadowCharacters[i - 1]) {
continue;
@@ -1679,6 +1679,7 @@ Object.defineProperty(exports, "__esModule", {
});
exports.SHOULD_NOT_BE_REACHED = exports.RuleName = exports.Rule = exports.ParserError = exports.NamedCharacterClass = exports.Identifier = exports.CustomCharacterClass = void 0;
exports.parsePasswordRules = parsePasswordRules;
+/* eslint-disable no-var */
// Copyright (c) 2019 - 2020 Apple Inc. Licensed under MIT License.
/*
@@ -1731,7 +1732,6 @@ class Rule {
}
}
exports.Rule = Rule;
-;
class NamedCharacterClass {
constructor(name) {
console.assert(_isValidRequiredOrAllowedPropertyValueIdentifier(name));
@@ -1748,10 +1748,8 @@ class NamedCharacterClass {
}
}
exports.NamedCharacterClass = NamedCharacterClass;
-;
class ParserError extends Error {}
exports.ParserError = ParserError;
-;
class CustomCharacterClass {
constructor(characters) {
console.assert(characters instanceof Array);
@@ -1767,14 +1765,11 @@ class CustomCharacterClass {
return `[${this._characters.join('').replace('"', '"')}]`;
}
}
-exports.CustomCharacterClass = CustomCharacterClass;
-;
// MARK: Lexer functions
-
+exports.CustomCharacterClass = CustomCharacterClass;
function _isIdentifierCharacter(c) {
console.assert(c.length === 1);
- // eslint-disable-next-line no-mixed-operators
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c === '-';
}
function _isASCIIDigit(c) {
@@ -1820,14 +1815,14 @@ function _markBitsForNamedCharacterClass(bitSet, namedCharacterClass) {
}
}
function _markBitsForCustomCharacterClass(bitSet, customCharacterClass) {
- for (let character of customCharacterClass.characters) {
+ for (const character of customCharacterClass.characters) {
bitSet[_bitSetIndexForCharacter(character)] = true;
}
}
function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFormatCompliant) {
// @ts-ignore
- let asciiPrintableBitSet = new Array('~'.codePointAt(0) - ' '.codePointAt(0) + 1);
- for (let propertyValue of propertyValues) {
+ const asciiPrintableBitSet = new Array('~'.codePointAt(0) - ' '.codePointAt(0) + 1);
+ for (const propertyValue of propertyValues) {
if (propertyValue instanceof NamedCharacterClass) {
if (propertyValue.name === Identifier.UNICODE) {
return [new NamedCharacterClass(Identifier.UNICODE)];
@@ -1842,32 +1837,32 @@ function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFo
}
let charactersSeen = [];
function checkRange(start, end) {
- let temp = [];
+ const temp = [];
for (let i = _bitSetIndexForCharacter(start); i <= _bitSetIndexForCharacter(end); ++i) {
if (asciiPrintableBitSet[i]) {
temp.push(_characterAtBitSetIndex(i));
}
}
- let result = temp.length === _bitSetIndexForCharacter(end) - _bitSetIndexForCharacter(start) + 1;
+ const result = temp.length === _bitSetIndexForCharacter(end) - _bitSetIndexForCharacter(start) + 1;
if (!result) {
charactersSeen = charactersSeen.concat(temp);
}
return result;
}
- let hasAllUpper = checkRange('A', 'Z');
- let hasAllLower = checkRange('a', 'z');
- let hasAllDigits = checkRange('0', '9');
+ const hasAllUpper = checkRange('A', 'Z');
+ const hasAllLower = checkRange('a', 'z');
+ const hasAllDigits = checkRange('0', '9');
// Check for special characters, accounting for characters that are given special treatment (i.e. '-' and ']')
let hasAllSpecial = false;
let hasDash = false;
let hasRightSquareBracket = false;
- let temp = [];
+ const temp = [];
for (let i = _bitSetIndexForCharacter(' '); i <= _bitSetIndexForCharacter('/'); ++i) {
if (!asciiPrintableBitSet[i]) {
continue;
}
- let character = _characterAtBitSetIndex(i);
+ const character = _characterAtBitSetIndex(i);
if (keepCustomCharacterClassFormatCompliant && character === '-') {
hasDash = true;
} else {
@@ -1883,7 +1878,7 @@ function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFo
if (!asciiPrintableBitSet[i]) {
continue;
}
- let character = _characterAtBitSetIndex(i);
+ const character = _characterAtBitSetIndex(i);
if (keepCustomCharacterClassFormatCompliant && character === ']') {
hasRightSquareBracket = true;
} else {
@@ -1901,12 +1896,12 @@ function _canonicalizedPropertyValues(propertyValues, keepCustomCharacterClassFo
if (hasRightSquareBracket) {
temp.push(']');
}
- let numberOfSpecialCharacters = _bitSetIndexForCharacter('/') - _bitSetIndexForCharacter(' ') + 1 + (_bitSetIndexForCharacter('@') - _bitSetIndexForCharacter(':') + 1) + (_bitSetIndexForCharacter('`') - _bitSetIndexForCharacter('[') + 1) + (_bitSetIndexForCharacter('~') - _bitSetIndexForCharacter('{') + 1);
+ const numberOfSpecialCharacters = _bitSetIndexForCharacter('/') - _bitSetIndexForCharacter(' ') + 1 + (_bitSetIndexForCharacter('@') - _bitSetIndexForCharacter(':') + 1) + (_bitSetIndexForCharacter('`') - _bitSetIndexForCharacter('[') + 1) + (_bitSetIndexForCharacter('~') - _bitSetIndexForCharacter('{') + 1);
hasAllSpecial = temp.length === numberOfSpecialCharacters;
if (!hasAllSpecial) {
charactersSeen = charactersSeen.concat(temp);
}
- let result = [];
+ const result = [];
if (hasAllUpper && hasAllLower && hasAllDigits && hasAllSpecial) {
return [new NamedCharacterClass(Identifier.ASCII_PRINTABLE)];
}
@@ -1934,7 +1929,7 @@ function _indexOfNonWhitespaceCharacter(input) {
let position = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
console.assert(position >= 0);
console.assert(position <= input.length);
- let length = input.length;
+ const length = input.length;
while (position < length && _isASCIIWhitespace(input[position])) {
++position;
}
@@ -1944,10 +1939,10 @@ function _parseIdentifier(input, position) {
console.assert(position >= 0);
console.assert(position < input.length);
console.assert(_isIdentifierCharacter(input[position]));
- let length = input.length;
- let seenIdentifiers = [];
+ const length = input.length;
+ const seenIdentifiers = [];
do {
- let c = input[position];
+ const c = input[position];
if (!_isIdentifierCharacter(c)) {
break;
}
@@ -1963,16 +1958,16 @@ function _parseCustomCharacterClass(input, position) {
console.assert(position >= 0);
console.assert(position < input.length);
console.assert(input[position] === CHARACTER_CLASS_START_SENTINEL);
- let length = input.length;
+ const length = input.length;
++position;
if (position >= length) {
// console.error('Found end-of-line instead of character class character')
return [null, position];
}
- let initialPosition = position;
- let result = [];
+ const initialPosition = position;
+ const result = [];
do {
- let c = input[position];
+ const c = input[position];
if (!_isASCIIPrintableCharacter(c)) {
++position;
continue;
@@ -2008,11 +2003,11 @@ function _parseCustomCharacterClass(input, position) {
function _parsePasswordRequiredOrAllowedPropertyValue(input, position) {
console.assert(position >= 0);
console.assert(position < input.length);
- let length = input.length;
- let propertyValues = [];
+ const length = input.length;
+ const propertyValues = [];
while (true) {
if (_isIdentifierCharacter(input[position])) {
- let identifierStartPosition = position;
+ const identifierStartPosition = position;
// eslint-disable-next-line no-redeclare
var [propertyValue, position] = _parseIdentifier(input, position);
if (!_isValidRequiredOrAllowedPropertyValueIdentifier(propertyValue)) {
@@ -2059,8 +2054,8 @@ function _parsePasswordRule(input, position) {
console.assert(position >= 0);
console.assert(position < input.length);
console.assert(_isIdentifierCharacter(input[position]));
- let length = input.length;
- var mayBeIdentifierStartPosition = position;
+ const length = input.length;
+ const mayBeIdentifierStartPosition = position;
// eslint-disable-next-line no-redeclare
var [identifier, position] = _parseIdentifier(input, position);
if (!Object.values(RuleName).includes(identifier)) {
@@ -2075,7 +2070,7 @@ function _parsePasswordRule(input, position) {
// console.error('Failed to find start of property value: ' + input.substr(position))
return [null, position, undefined];
}
- let property = {
+ const property = {
name: identifier,
value: null
};
@@ -2131,7 +2126,7 @@ function _parseInteger(input, position) {
// console.error('Failed to parse value of type integer; not a number: ' + input.substr(position))
return [null, position];
}
- let length = input.length;
+ const length = input.length;
// let initialPosition = position
let result = 0;
do {
@@ -2152,8 +2147,8 @@ function _parseInteger(input, position) {
* @private
*/
function _parsePasswordRulesInternal(input) {
- let parsedProperties = [];
- let length = input.length;
+ const parsedProperties = [];
+ const length = input.length;
var position = _indexOfNonWhitespaceCharacter(input);
while (position < length) {
if (!_isIdentifierCharacter(input[position])) {
@@ -2190,7 +2185,7 @@ function _parsePasswordRulesInternal(input) {
* @returns {Rule[]}
*/
function parsePasswordRules(input, formatRulesForMinifiedVersion) {
- let [passwordRules, maybeMessage] = _parsePasswordRulesInternal(input);
+ const [passwordRules, maybeMessage] = _parsePasswordRulesInternal(input);
if (!passwordRules) {
throw new ParserError(maybeMessage);
}
@@ -2200,13 +2195,13 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) {
// When formatting rules for minified version, we should keep the formatted rules
// as similar to the input as possible. Avoid copying required rules to allowed rules.
- let suppressCopyingRequiredToAllowed = formatRulesForMinifiedVersion;
- let requiredRules = [];
+ const suppressCopyingRequiredToAllowed = formatRulesForMinifiedVersion;
+ const requiredRules = [];
let newAllowedValues = [];
let minimumMaximumConsecutiveCharacters = null;
let maximumMinLength = 0;
let minimumMaxLength = null;
- for (let rule of passwordRules) {
+ for (const rule of passwordRules) {
switch (rule.name) {
case RuleName.MAX_CONSECUTIVE:
minimumMaximumConsecutiveCharacters = minimumMaximumConsecutiveCharacters ? Math.min(rule.value, minimumMaximumConsecutiveCharacters) : rule.value;
@@ -2239,10 +2234,10 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) {
if (minimumMaximumConsecutiveCharacters !== null) {
newPasswordRules.push(new Rule(RuleName.MAX_CONSECUTIVE, minimumMaximumConsecutiveCharacters));
}
- let sortedRequiredRules = requiredRules.sort(function (a, b) {
+ const sortedRequiredRules = requiredRules.sort(function (a, b) {
const namedCharacterClassOrder = [Identifier.LOWER, Identifier.UPPER, Identifier.DIGIT, Identifier.SPECIAL, Identifier.ASCII_PRINTABLE, Identifier.UNICODE];
- let aIsJustOneNamedCharacterClass = a.value.length === 1 && a.value[0] instanceof NamedCharacterClass;
- let bIsJustOneNamedCharacterClass = b.value.length === 1 && b.value[0] instanceof NamedCharacterClass;
+ const aIsJustOneNamedCharacterClass = a.value.length === 1 && a.value[0] instanceof NamedCharacterClass;
+ const bIsJustOneNamedCharacterClass = b.value.length === 1 && b.value[0] instanceof NamedCharacterClass;
if (aIsJustOneNamedCharacterClass && !bIsJustOneNamedCharacterClass) {
return -1;
}
@@ -2250,8 +2245,8 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) {
return 1;
}
if (aIsJustOneNamedCharacterClass && bIsJustOneNamedCharacterClass) {
- let aIndex = namedCharacterClassOrder.indexOf(a.value[0].name);
- let bIndex = namedCharacterClassOrder.indexOf(b.value[0].name);
+ const aIndex = namedCharacterClassOrder.indexOf(a.value[0].name);
+ const bIndex = namedCharacterClassOrder.indexOf(b.value[0].name);
return aIndex - bIndex;
}
return 0;
@@ -2896,6 +2891,9 @@ module.exports={
"keldoc.com": {
"password-rules": "minlength: 12; required: lower; required: upper; required: digit; required: [!@#$%^&*];"
},
+ "kennedy-center.org": {
+ "password-rules": "minlength: 8; required: lower; required: upper; required: digit; required: [!#$%&*?@];"
+ },
"key.harvard.edu": {
"password-rules": "minlength: 10; maxlength: 100; required: lower; required: upper; required: digit; allowed: [-@_#!&$`%*+()./,;~:{}|?>=<^[']];"
},
@@ -3408,8 +3406,8 @@ class CredentialsImport {
activeInput?.focus();
}
async started() {
- this.device.deviceApi.notify(new _deviceApiCalls.CloseAutofillParentCall(null));
this.device.deviceApi.notify(new _deviceApiCalls.StartCredentialsImportFlowCall({}));
+ this.device.deviceApi.notify(new _deviceApiCalls.CloseAutofillParentCall(null));
}
async dismissed() {
this.device.deviceApi.notify(new _deviceApiCalls.CredentialsImportFlowPermanentlyDismissedCall(null));
@@ -3453,7 +3451,7 @@ function createDevice() {
};
// Create the DeviceAPI + Setting
- let deviceApi = new _index.DeviceApi(globalConfig.isDDGTestMode ? loggingTransport : transport);
+ const deviceApi = new _index.DeviceApi(globalConfig.isDDGTestMode ? loggingTransport : transport);
const settings = new _Settings.Settings(globalConfig, deviceApi);
if (globalConfig.isWindows) {
if (globalConfig.isTopFrame) {
@@ -3587,9 +3585,9 @@ class AndroidInterface extends _InterfacePrototype.default {
}
/**
- * Used by the email web app
- * Provides functionality to log the user out
- */
+ * Used by the email web app
+ * Provides functionality to log the user out
+ */
removeUserData() {
try {
return window.EmailInterface.removeCredentials();
@@ -4858,14 +4856,16 @@ class InterfacePrototype {
});
break;
default:
- // Also fire pixel when filling an identity with the personal duck address from an email field
- const checks = [subtype === 'emailAddress', this.hasLocalAddresses, data?.emailAddress === (0, _autofillUtils.formatDuckAddress)(this.#addresses.personalAddress)];
- if (checks.every(Boolean)) {
- this.firePixel({
- pixelName: 'autofill_personal_address'
- });
+ {
+ // Also fire pixel when filling an identity with the personal duck address from an email field
+ const checks = [subtype === 'emailAddress', this.hasLocalAddresses, data?.emailAddress === (0, _autofillUtils.formatDuckAddress)(this.#addresses.personalAddress)];
+ if (checks.every(Boolean)) {
+ this.firePixel({
+ pixelName: 'autofill_personal_address'
+ });
+ }
+ break;
}
- break;
}
}
// some platforms do not include a `success` object, why?
@@ -5113,13 +5113,15 @@ class InterfacePrototype {
postSubmit(values, form) {
if (!form.form) return;
if (!form.hasValues(values)) return;
- const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated];
+ const shouldTriggerPartialSave = Object.keys(values?.credentials || {}).length === 1 && Boolean(values?.credentials?.username) && this.settings.featureToggles.partial_form_saves;
+ const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, shouldTriggerPartialSave];
if (checks.some(Boolean)) {
const formData = (0, _Credentials.appendGeneratedKey)(values, {
password: this.passwordGenerator.password,
username: this.emailProtection.lastGenerated
});
- this.storeFormData(formData, 'formSubmission');
+ const trigger = shouldTriggerPartialSave ? 'partialSave' : 'formSubmission';
+ this.storeFormData(formData, trigger);
}
}
@@ -5540,6 +5542,7 @@ function initFormSubmissionsApi(forms, matching) {
// @ts-ignore
if (btns.find(btn => btn.contains(realTarget))) return true;
+ return false;
});
matchingForm?.submitHandler('global pointerdown event + matching form');
if (!matchingForm) {
@@ -5635,7 +5638,7 @@ function overlayApi(device) {
* @returns {Promise}
*/
async selectedDetail(data, type) {
- let detailsEntries = Object.entries(data).map(_ref => {
+ const detailsEntries = Object.entries(data).map(_ref => {
let [key, value] = _ref;
return [key, String(value)];
});
@@ -5898,7 +5901,7 @@ class Form {
*/
getValuesReadyForStorage() {
const formValues = this.getRawValues();
- return (0, _formatters.prepareFormValuesForStorage)(formValues);
+ return (0, _formatters.prepareFormValuesForStorage)(formValues, this.device.settings.featureToggles.partial_form_saves);
}
/**
@@ -5929,7 +5932,7 @@ class Form {
if (!input.classList.contains('ddg-autofilled')) return;
(0, _autofillUtils.removeInlineStyles)(input, (0, _inputStyles.getIconStylesAutofilled)(input, this));
(0, _autofillUtils.removeInlineStyles)(input, {
- 'cursor': 'pointer'
+ cursor: 'pointer'
});
input.classList.remove('ddg-autofilled');
this.addAutofillStyles(input);
@@ -6050,20 +6053,10 @@ class Form {
if (this.form.matches(selector)) {
this.addInput(this.form);
} else {
- /** @type {Element[] | NodeList} */
- let foundInputs = [];
- // Some sites seem to be overriding `form.elements`, so we need to check if it's still iterable.
- if (this.form instanceof HTMLFormElement && this.form.elements != null && Symbol.iterator in Object(this.form.elements)) {
- // For form elements we use .elements to catch fields outside the form itself using the form attribute.
- // It also catches all elements when the markup is broken.
- // We use .filter to avoid fieldset, button, textarea etc.
- const formElements = [...this.form.elements].filter(el => el.matches(selector));
- // If there are no form elements, we try to look for all
- // enclosed elements within the form.
- foundInputs = formElements.length > 0 ? formElements : (0, _autofillUtils.findEnclosedElements)(this.form, selector);
- } else {
- foundInputs = this.form.querySelectorAll(selector);
- }
+ // Attempt to get form's control elements first as it can catch elements when markup is broke, or if the fields are outside the form.
+ // Other wise use queryElementsWithShadow, that can scan for shadow tree.
+ const formControlElements = (0, _autofillUtils.getFormControlElements)(this.form, selector);
+ const foundInputs = formControlElements != null ? [...formControlElements, ...(0, _autofillUtils.findElementsInShadowTree)(this.form, selector)] : (0, _autofillUtils.queryElementsWithShadow)(this.form, selector, true);
if (foundInputs.length < MAX_INPUTS_PER_FORM) {
foundInputs.forEach(input => this.addInput(input));
} else {
@@ -6124,7 +6117,7 @@ class Form {
}
get submitButtons() {
const selector = this.matching.cssSelector('submitButtonSelector');
- const allButtons = /** @type {HTMLElement[]} */(0, _autofillUtils.findEnclosedElements)(this.form, selector);
+ const allButtons = /** @type {HTMLElement[]} */(0, _autofillUtils.queryElementsWithShadow)(this.form, selector);
return allButtons.filter(btn => (0, _autofillUtils.isPotentiallyViewable)(btn) && (0, _autofillUtils.isLikelyASubmitButton)(btn, this.matching) && (0, _autofillUtils.buttonMatchesFormType)(btn, this));
}
attemptSubmissionIfNeeded() {
@@ -6247,12 +6240,12 @@ class Form {
if ((0, _autofillUtils.wasAutofilledByChrome)(input)) return;
if ((0, _autofillUtils.isEventWithinDax)(e, e.target)) {
(0, _autofillUtils.addInlineStyles)(e.target, {
- 'cursor': 'pointer',
+ cursor: 'pointer',
...onMouseMove
});
} else {
(0, _autofillUtils.removeInlineStyles)(e.target, {
- 'cursor': 'pointer'
+ cursor: 'pointer'
});
// Only overwrite active icon styles if tooltip is closed
if (!this.device.isTooltipActive()) {
@@ -6264,7 +6257,7 @@ class Form {
});
this.addListener(input, 'mouseleave', e => {
(0, _autofillUtils.removeInlineStyles)(e.target, {
- 'cursor': 'pointer'
+ cursor: 'pointer'
});
// Only overwrite active icon styles if tooltip is closed
if (!this.device.isTooltipActive()) {
@@ -6362,7 +6355,7 @@ class Form {
this.touched.add(input);
this.device.attachTooltip({
form: this,
- input: input,
+ input,
click: clickCoords,
trigger: 'userInitiated',
triggerMetaData: {
@@ -6591,7 +6584,7 @@ class Form {
}, 'credentials');
this.device.attachTooltip({
form: this,
- input: input,
+ input,
click: null,
trigger: 'autoprompt',
triggerMetaData: {
@@ -6826,6 +6819,23 @@ class FormAnalyzer {
}
});
}
+
+ /**
+ * Function that checks if the element is an external link or a custom web element that
+ * encapsulates a link.
+ * @param {any} el
+ * @returns {boolean}
+ */
+ isElementExternalLink(el) {
+ // Checks if the element is present in the cusotm elements registry and ends with a '-link' suffix.
+ // If it does, it checks if it contains an anchor element inside.
+ const tagName = el.nodeName.toLowerCase();
+ const isCustomWebElementLink = customElements?.get(tagName) != null && /-link$/.test(tagName) && (0, _autofillUtils.findElementsInShadowTree)(el, 'a').length > 0;
+
+ // if an external link matches one of the regexes, we assume the match is not pertinent to the current form
+ const isElementLink = el instanceof HTMLAnchorElement && el.href && el.getAttribute('href') !== '#' || (el.getAttribute('role') || '').toUpperCase() === 'LINK' || el.matches('button[class*=secondary]');
+ return isCustomWebElementLink || isElementLink;
+ }
evaluateElement(el) {
const string = (0, _autofillUtils.getTextShallow)(el);
if (el.matches(this.matching.cssSelector('password'))) {
@@ -6846,7 +6856,7 @@ class FormAnalyzer {
if (likelyASubmit) {
this.form.querySelectorAll('input[type=submit], button[type=submit]').forEach(submit => {
// If there is another element marked as submit and this is not, flip back to false
- if (el.type !== 'submit' && el !== submit) {
+ if (el.getAttribute('type') !== 'submit' && el !== submit) {
likelyASubmit = false;
}
});
@@ -6865,8 +6875,7 @@ class FormAnalyzer {
});
return;
}
- // if an external link matches one of the regexes, we assume the match is not pertinent to the current form
- if (el instanceof HTMLAnchorElement && el.href && el.getAttribute('href') !== '#' || (el.getAttribute('role') || '').toUpperCase() === 'LINK' || el.matches('button[class*=secondary]')) {
+ if (this.isElementExternalLink(el)) {
let shouldFlip = true;
let strength = 1;
// Don't flip forgotten password links
@@ -6885,9 +6894,10 @@ class FormAnalyzer {
});
} else {
// any other case
+ const isH1Element = el.tagName === 'H1';
this.updateSignal({
string,
- strength: 1,
+ strength: isH1Element ? 3 : 1,
signalType: `generic: ${string}`,
shouldCheckUnifiedForm: true
});
@@ -6905,7 +6915,7 @@ class FormAnalyzer {
// Check form contents (noisy elements are skipped with the safeUniversalSelector)
const selector = this.matching.cssSelector('safeUniversalSelector');
- const formElements = (0, _autofillUtils.findEnclosedElements)(this.form, selector);
+ const formElements = (0, _autofillUtils.queryElementsWithShadow)(this.form, selector);
for (let i = 0; i < formElements.length; i++) {
// Safety cutoff to avoid huge DOMs freezing the browser
if (i >= 200) break;
@@ -6965,7 +6975,7 @@ class FormAnalyzer {
}
// Match form textContent against common cc fields (includes hidden labels)
- const textMatches = formEl.textContent?.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig);
+ const textMatches = formEl.textContent?.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/gi);
// De-dupe matches to avoid counting the same element more than once
const deDupedMatches = new Set(textMatches?.map(match => match.toLowerCase()));
@@ -7284,7 +7294,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = {
Anguilla: 'AI',
Albania: 'AL',
Armenia: 'AM',
- 'Curaçao': 'CW',
+ Curaçao: 'CW',
Angola: 'AO',
Antarctica: 'AQ',
Argentina: 'AR',
@@ -7473,7 +7483,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = {
Paraguay: 'PY',
Qatar: 'QA',
'Outlying Oceania': 'QO',
- 'Réunion': 'RE',
+ Réunion: 'RE',
Zimbabwe: 'ZW',
Romania: 'RO',
Russia: 'SU',
@@ -7550,6 +7560,7 @@ Object.defineProperty(exports, "__esModule", {
exports.prepareFormValuesForStorage = exports.inferCountryCodeFromElement = exports.getUnifiedExpiryDate = exports.getMMAndYYYYFromString = exports.getCountryName = exports.getCountryDisplayName = exports.formatPhoneNumber = exports.formatFullName = exports.formatCCYear = void 0;
var _matching = require("./matching.js");
var _countryNames = require("./countryNames.js");
+var _autofillUtils = require("../autofill-utils.js");
// Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024
const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i;
// Matches 4 non-digit repeated characters (YYYY or AAAA) or 4 digits (2022)
@@ -7718,36 +7729,25 @@ const getMMAndYYYYFromString = expiration => {
};
/**
- * @param {InternalDataStorageObject} credentials
+ * @param {InternalDataStorageObject} data
* @return {boolean}
*/
exports.getMMAndYYYYFromString = getMMAndYYYYFromString;
-const shouldStoreCredentials = _ref3 => {
- let {
- credentials
- } = _ref3;
- return Boolean(credentials.password);
-};
-
-/**
- * @param {InternalDataStorageObject} credentials
- * @return {boolean}
- */
-const shouldStoreIdentities = _ref4 => {
+const shouldStoreIdentities = _ref3 => {
let {
identities
- } = _ref4;
+ } = _ref3;
return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity);
};
/**
- * @param {InternalDataStorageObject} credentials
+ * @param {InternalDataStorageObject} data
* @return {boolean}
*/
-const shouldStoreCreditCards = _ref5 => {
+const shouldStoreCreditCards = _ref4 => {
let {
creditCards
- } = _ref5;
+ } = _ref4;
if (!creditCards.cardNumber) return false;
if (creditCards.cardSecurityCode) return true;
// Some forms (Amazon) don't have the cvv, so we still save if there's the expiration
@@ -7770,7 +7770,8 @@ const formatPhoneNumber = phone => phone.replaceAll(/[^0-9|+]/g, '');
* @return {DataStorageObject}
*/
exports.formatPhoneNumber = formatPhoneNumber;
-const prepareFormValuesForStorage = formValues => {
+const prepareFormValuesForStorage = function (formValues) {
+ let canTriggerPartialSave = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
/** @type {Partial} */
let {
credentials,
@@ -7783,14 +7784,15 @@ const prepareFormValuesForStorage = formValues => {
creditCards.cardName = identities?.fullName || formatFullName(identities);
}
- /** Fixes for credentials **/
- // Don't store if there isn't enough data
- if (shouldStoreCredentials(formValues)) {
- // If we don't have a username to match a password, let's see if the email is available
- if (credentials.password && !credentials.username && identities.emailAddress) {
- credentials.username = identities.emailAddress;
- }
- } else {
+ /** Fixes for credentials */
+ // If we don't have a username to match a password, let's see if email or phone are available
+ if (credentials.password && !credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) {
+ // @ts-ignore - username will be likely undefined, but needs to be specifically assigned to a string value
+ credentials.username = identities.emailAddress || identities.phone;
+ }
+
+ // If there's no password, and we shouldn't trigger a partial save, let's discard the object
+ if (!credentials.password && !canTriggerPartialSave) {
credentials = undefined;
}
@@ -7846,7 +7848,7 @@ const prepareFormValuesForStorage = formValues => {
};
exports.prepareFormValuesForStorage = prepareFormValuesForStorage;
-},{"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){
+},{"../autofill-utils.js":54,"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -7889,7 +7891,7 @@ const getBasicStyles = (input, icon) => ({
'background-repeat': 'no-repeat',
'background-origin': 'content-box',
'background-image': `url(${icon})`,
- 'transition': 'background 0s'
+ transition: 'background 0s'
});
/**
@@ -7932,7 +7934,7 @@ const getIconStylesAutofilled = (input, form) => {
return {
...iconStyle,
'background-color': '#F8F498',
- 'color': '#333333'
+ color: '#333333'
};
};
exports.getIconStylesAutofilled = getIconStylesAutofilled;
@@ -8220,14 +8222,14 @@ const extractElementStrings = element => {
// only take the string when it's an explicit text node
if (el.nodeType === el.TEXT_NODE || !el.childNodes.length) {
- let trimmedText = (0, _matching.removeExcessWhitespace)(el.textContent);
+ const trimmedText = (0, _matching.removeExcessWhitespace)(el.textContent);
if (trimmedText) {
strings.add(trimmedText);
}
return;
}
- for (let node of el.childNodes) {
- let nodeType = node.nodeType;
+ for (const node of el.childNodes) {
+ const nodeType = node.nodeType;
if (nodeType !== node.ELEMENT_NODE && nodeType !== node.TEXT_NODE) {
continue;
}
@@ -8629,7 +8631,7 @@ const matchingConfiguration = exports.matchingConfiguration = {
match: /sign.?up|join|register|enroll|(create|new).+account|newsletter|subscri(be|ption)|settings|preferences|profile|update|iscri(viti|zione)|registra(ti|zione)|(?:nuovo|crea(?:zione)?) account|contatt(?:ac)?i|sottoscriv|sottoscrizione|impostazioni|preferenze|aggiorna|anmeld(en|ung)|registrier(en|ung)|neukunde|neuer (kunde|benutzer|nutzer)|registreren|eigenschappen|profiel|bijwerken|s.inscrire|inscription|s.abonner|abonnement|préférences|profil|créer un compte|regis(trarse|tro)|regÃstrate|inscr(ibirse|ipción|Ãbete)|crea(r cuenta)?|nueva cuenta|nuevo (cliente|usuario)|preferencias|perfil|lista de correo|registrer(a|ing)|(nytt|öppna) konto|nyhetsbrev|prenumer(era|ation)|kontakt|skapa|starta|inställningar|min (sida|kundvagn)|uppdatera/iu
},
resetPasswordLink: {
- match: /(forgot(ten)?|reset|don't remember) (your )?password|password forgotten|password dimenticata|reset(?:ta) password|recuper[ao] password|(vergessen|verloren|verlegt|wiederherstellen) passwort|wachtwoord (vergeten|reset)|(oublié|récupérer) ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)|re(iniciar|cuperar) (contraseña|clave)|olvid(ó su|aste tu|é mi) (contraseña|clave)|recordar( su)? (contraseña|clave)|glömt lösenord|återställ lösenord/iu
+ match: /(forgot(ten)?|reset|don't remember).?(your )?password|password forgotten|password dimenticata|reset(?:ta) password|recuper[ao] password|(vergessen|verloren|verlegt|wiederherstellen) passwort|wachtwoord (vergeten|reset)|(oublié|récupérer) ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)|re(iniciar|cuperar) (contraseña|clave)|olvid(ó su|aste tu|é mi) (contraseña|clave)|recordar( su)? (contraseña|clave)|glömt lösenord|återställ lösenord/iu
},
loginProvidersRegex: {
match: / with | con | mit | met | avec /iu
@@ -8894,8 +8896,8 @@ class Matching {
*
* `email: [{type: "email", strategies: {cssSelector: "email", ... etc}]`
*/
- for (let [listName, matcherNames] of Object.entries(this.#config.matchers.lists)) {
- for (let fieldName of matcherNames) {
+ for (const [listName, matcherNames] of Object.entries(this.#config.matchers.lists)) {
+ for (const fieldName of matcherNames) {
if (!this.#matcherLists[listName]) {
this.#matcherLists[listName] = [];
}
@@ -9012,7 +9014,7 @@ class Matching {
* @type {string[]}
*/
const selectors = [];
- for (let matcher of matcherList) {
+ for (const matcher of matcherList) {
if (matcher.strategies.cssSelector) {
const css = this.cssSelector(matcher.strategies.cssSelector);
if (css) {
@@ -9149,12 +9151,12 @@ class Matching {
/**
* Loop through each strategy in order
*/
- for (let strategyName of this.#defaultStrategyOrder) {
+ for (const strategyName of this.#defaultStrategyOrder) {
let result;
/**
* Now loop through each matcher in the list.
*/
- for (let matcher of matchers) {
+ for (const matcher of matchers) {
/**
* for each `strategyName` (such as cssSelector), check
* if the current matcher implements it.
@@ -9279,16 +9281,16 @@ class Matching {
if (!ddgMatcher || !ddgMatcher.match) {
return defaultResult;
}
- let matchRexExp = this.getDDGMatcherRegex(lookup);
+ const matchRexExp = this.getDDGMatcherRegex(lookup);
if (!matchRexExp) {
return defaultResult;
}
- let requiredScore = ['match', 'forceUnknown', 'maxDigits'].filter(ddgMatcherProp => ddgMatcherProp in ddgMatcher).length;
+ const requiredScore = ['match', 'forceUnknown', 'maxDigits'].filter(ddgMatcherProp => ddgMatcherProp in ddgMatcher).length;
/** @type {MatchableStrings[]} */
const matchableStrings = ddgMatcher.matchableStrings || ['labelText', 'placeholderAttr', 'relatedText'];
- for (let stringName of matchableStrings) {
- let elementString = this.activeElementStrings[stringName];
+ for (const stringName of matchableStrings) {
+ const elementString = this.activeElementStrings[stringName];
if (!elementString) continue;
// Scoring to ensure all DDG tests are valid
@@ -9304,7 +9306,7 @@ class Matching {
// If a negated regex was provided, ensure it does not match
// If it DOES match - then we need to prevent any future strategies from continuing
if (ddgMatcher.forceUnknown) {
- let notRegex = ddgMatcher.forceUnknown;
+ const notRegex = ddgMatcher.forceUnknown;
if (!notRegex) {
return {
...result,
@@ -9323,7 +9325,7 @@ class Matching {
}
}
if (ddgMatcher.skip) {
- let skipRegex = ddgMatcher.skip;
+ const skipRegex = ddgMatcher.skip;
if (!skipRegex) {
return {
...result,
@@ -9388,8 +9390,8 @@ class Matching {
}
/** @type {MatchableStrings[]} */
const stringsToMatch = ['placeholderAttr', 'nameAttr', 'labelText', 'id', 'relatedText'];
- for (let stringName of stringsToMatch) {
- let elementString = this.activeElementStrings[stringName];
+ for (const stringName of stringsToMatch) {
+ const elementString = this.activeElementStrings[stringName];
if (!elementString) continue;
if ((0, _autofillUtils.safeRegexTest)(regex, elementString)) {
return {
@@ -9463,14 +9465,14 @@ class Matching {
fields: {}
},
strategies: {
- 'vendorRegex': {
+ vendorRegex: {
rules: {},
ruleSets: []
},
- 'ddgMatcher': {
+ ddgMatcher: {
matchers: {}
},
- 'cssSelector': {
+ cssSelector: {
selectors: {}
}
}
@@ -9641,7 +9643,7 @@ const removeExcessWhitespace = function () {
exports.removeExcessWhitespace = removeExcessWhitespace;
const getExplicitLabelsText = el => {
const labelTextCandidates = [];
- for (let label of el.labels || []) {
+ for (const label of el.labels || []) {
labelTextCandidates.push(...(0, _labelUtil.extractElementStrings)(label));
}
if (el.hasAttribute('aria-label')) {
@@ -9696,7 +9698,7 @@ const getRelatedText = (el, form, cssSelector) => {
// If we didn't find a container, try looking for an adjacent label
if (scope === el) {
- let previousEl = recursiveGetPreviousElSibling(el);
+ const previousEl = recursiveGetPreviousElSibling(el);
if (previousEl instanceof HTMLElement) {
scope = previousEl;
}
@@ -10355,19 +10357,18 @@ class DefaultScanner {
if (this.device.globalConfig.isDDGDomain) {
return this;
}
- if ('matches' in context && context.matches?.(this.matching.cssSelector('formInputsSelectorWithoutSelect'))) {
+ const formInputsSelectorWithoutSelect = this.matching.cssSelector('formInputsSelectorWithoutSelect');
+ if ('matches' in context && context.matches?.(formInputsSelectorWithoutSelect)) {
this.addInput(context);
} else {
- const selector = this.matching.cssSelector('formInputsSelectorWithoutSelect');
- const inputs = context.querySelectorAll(selector);
+ const inputs = context.querySelectorAll(formInputsSelectorWithoutSelect);
if (inputs.length > this.options.maxInputsPerPage) {
this.setMode('stopped', `Too many input fields in the given context (${inputs.length}), stop scanning`, context);
return this;
}
inputs.forEach(input => this.addInput(input));
if (context instanceof HTMLFormElement && this.forms.get(context)?.hasShadowTree) {
- const selector = this.matching.cssSelector('formInputsSelectorWithoutSelect');
- (0, _autofillUtils.findEnclosedElements)(context, selector).forEach(input => {
+ (0, _autofillUtils.findElementsInShadowTree)(context, formInputsSelectorWithoutSelect).forEach(input => {
if (input instanceof HTMLInputElement) {
this.addInput(input, context);
}
@@ -10453,12 +10454,16 @@ class DefaultScanner {
}
if (element.parentElement) {
element = element.parentElement;
- const inputs = element.querySelectorAll(this.matching.cssSelector('formInputsSelector'));
- const buttons = element.querySelectorAll(this.matching.cssSelector('submitButtonSelector'));
- // If we find a button or another input, we assume that's our form
- if (inputs.length > 1 || buttons.length) {
- // found related input, return common ancestor
- return element;
+ // If the parent is a redundant component (only contains a single element or is a shadowRoot) do not increase the traversal count.
+ if (element.childElementCount > 1) {
+ const inputs = element.querySelectorAll(this.matching.cssSelector('formInputsSelector'));
+ const buttons = element.querySelectorAll(this.matching.cssSelector('submitButtonSelector'));
+ // If we find a button or another input, we assume that's our form
+ if (inputs.length > 1 || buttons.length) {
+ // found related input, return common ancestor
+ return element;
+ }
+ traversalLayerCount++;
}
} else {
// possibly a shadow boundary, so traverse through the shadow root and find the form
@@ -10466,9 +10471,11 @@ class DefaultScanner {
if (root instanceof ShadowRoot && root.host) {
// @ts-ignore
element = root.host;
+ } else {
+ // We're in a strange state (no parent or shadow root), just break out of the loop for safety
+ break;
}
}
- traversalLayerCount++;
}
return input;
}
@@ -10551,7 +10558,7 @@ class DefaultScanner {
this.changedElements.clear();
} else if (!this.rescanAll) {
// otherwise keep adding each element to the queue
- for (let element of htmlElements) {
+ for (const element of htmlElements) {
this.changedElements.add(element);
}
}
@@ -10575,7 +10582,7 @@ class DefaultScanner {
this.findEligibleInputs(document);
return;
}
- for (let element of this.changedElements) {
+ for (const element of this.changedElements) {
if (element.isConnected) {
this.findEligibleInputs(element);
}
@@ -10596,7 +10603,7 @@ class DefaultScanner {
const outgoing = [];
for (const mutationRecord of mutationList) {
if (mutationRecord.type === 'childList') {
- for (let addedNode of mutationRecord.addedNodes) {
+ for (const addedNode of mutationRecord.addedNodes) {
if (!(addedNode instanceof HTMLElement)) continue;
if (addedNode.nodeName === 'DDG-AUTOFILL') continue;
outgoing.push(addedNode);
@@ -10630,12 +10637,13 @@ class DefaultScanner {
// find the enclosing parent form, and scan it.
if (realTarget instanceof HTMLInputElement && !realTarget.hasAttribute(ATTR_INPUT_TYPE)) {
const parentForm = this.getParentForm(realTarget);
- if (parentForm && parentForm instanceof HTMLFormElement) {
- const hasShadowTree = event.target?.shadowRoot != null;
- const form = new _Form.Form(parentForm, realTarget, this.device, this.matching, this.shouldAutoprompt, hasShadowTree);
- this.forms.set(parentForm, form);
- this.findEligibleInputs(parentForm);
- }
+
+ // If the parent form is an input element we bail.
+ if (parentForm instanceof HTMLInputElement) return;
+ const hasShadowTree = event.target?.shadowRoot != null;
+ const form = new _Form.Form(parentForm, realTarget, this.device, this.matching, this.shouldAutoprompt, hasShadowTree);
+ this.forms.set(parentForm, form);
+ this.findEligibleInputs(parentForm);
}
window.performance?.mark?.('scan_shadow:init:end');
(0, _autofillUtils.logPerformance)('scan_shadow');
@@ -11010,7 +11018,8 @@ class Settings {
inputType_credentials: false,
inputType_creditCards: false,
inlineIcon_credentials: false,
- unknown_username_categorization: false
+ unknown_username_categorization: false,
+ partial_form_saves: false
},
/** @type {AvailableInputTypes} */
availableInputTypes: {
@@ -11334,7 +11343,7 @@ ${css}
if (btn.matches('.wrapper:not(.top-autofill) button:hover, .currentFocus')) {
callbacks.onSelect(btn.id);
} else {
- console.warn('The button doesn\'t seem to be hovered. Please check.');
+ console.warn("The button doesn't seem to be hovered. Please check.");
}
});
});
@@ -11538,7 +11547,9 @@ const defaultOptions = exports.defaultOptions = {
}`,
css: ``,
setSize: undefined,
- remove: () => {/** noop */},
+ remove: () => {
+ /** noop */
+ },
testMode: false,
checkVisibility: true,
hasCaret: false,
@@ -11566,9 +11577,9 @@ class HTMLTooltip {
this.tooltip = null;
this.getPosition = getPosition;
const forcedVisibilityStyles = {
- 'display': 'block',
- 'visibility': 'visible',
- 'opacity': '1'
+ display: 'block',
+ visibility: 'visible',
+ opacity: '1'
};
// @ts-ignore how to narrow this.host to HTMLElement?
(0, _autofillUtils.addInlineStyles)(this.host, forcedVisibilityStyles);
@@ -11826,7 +11837,7 @@ class HTMLTooltip {
checkVisibility: this.options.checkVisibility
});
} else {
- console.warn('The button doesn\'t seem to be hovered. Please check.');
+ console.warn("The button doesn't seem to be hovered. Please check.");
}
}
}
@@ -12208,25 +12219,27 @@ class HTMLTooltipUIController extends _UIController.UIController {
/**
* Called when clicking on the Manage… button in the html tooltip
- *
* @param {SupportedMainTypes} type
* @returns {*}
* @private
*/
_onManage(type) {
- this.removeTooltip();
switch (type) {
case 'credentials':
- return this._options.device.openManagePasswords();
+ this._options.device.openManagePasswords();
+ break;
case 'creditCards':
- return this._options.device.openManageCreditCards();
+ this._options.device.openManageCreditCards();
+ break;
case 'identities':
- return this._options.device.openManageIdentities();
+ this._options.device.openManageIdentities();
+ break;
default:
// noop
}
- }
+ this.removeTooltip();
+ }
_onIncontextSignupDismissed(_ref) {
let {
hasOtherOptions
@@ -12770,10 +12783,14 @@ Object.defineProperty(exports, "__esModule", {
});
exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0;
exports.escapeXML = escapeXML;
-exports.findEnclosedElements = findEnclosedElements;
+exports.findElementsInShadowTree = findElementsInShadowTree;
exports.formatDuckAddress = void 0;
exports.getActiveElement = getActiveElement;
-exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = exports.getDaxBoundingBox = void 0;
+exports.getDaxBoundingBox = void 0;
+exports.getFormControlElements = getFormControlElements;
+exports.getTextShallow = void 0;
+exports.hasUsernameLikeIdentity = hasUsernameLikeIdentity;
+exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = void 0;
exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper;
exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0;
exports.isLocalNetwork = isLocalNetwork;
@@ -12782,6 +12799,7 @@ exports.isValidTLD = isValidTLD;
exports.logPerformance = logPerformance;
exports.notifyWebApp = void 0;
exports.pierceShadowTree = pierceShadowTree;
+exports.queryElementsWithShadow = queryElementsWithShadow;
exports.safeExecute = exports.removeInlineStyles = void 0;
exports.safeRegexTest = safeRegexTest;
exports.setValue = exports.sendAndWaitForAnswer = void 0;
@@ -13154,8 +13172,9 @@ const isLikelyASubmitButton = (el, matching) => {
// has high-signal submit classes
safeRegexTest(/submit/i, dataTestId) || safeRegexTest(matching.getDDGMatcherRegex('submitButtonRegex'), text) ||
// has high-signal text
- el.offsetHeight * el.offsetWidth >= 10000 && !safeRegexTest(/secondary/i, el.className) // it's a large element 250x40px
- ) && el.offsetHeight * el.offsetWidth >= 2000 &&
+ el.offsetHeight * el.offsetWidth >= 10000 && !safeRegexTest(/secondary/i, el.className)) &&
+ // it's a large element 250x40px
+ el.offsetHeight * el.offsetWidth >= 2000 &&
// it's not a very small button like inline links and such
!safeRegexTest(matching.getDDGMatcherRegex('submitButtonUnlikelyRegex'), text + ' ' + ariaLabel);
};
@@ -13383,22 +13402,16 @@ function getActiveElement() {
}
/**
- * Takes a root element and tries to find visible elements first, and if it fails, it tries to find shadow elements
+ * Takes a root element and tries to find elements in shadow DOMs that match the selector
* @param {HTMLElement|HTMLFormElement} root
* @param {string} selector
* @returns {Element[]}
*/
-function findEnclosedElements(root, selector) {
- // Check if there are any normal elements that match the selector
- const elements = root.querySelectorAll(selector);
- if (elements.length > 0) {
- return Array.from(elements);
- }
-
- // Check if there are any shadow elements that match the selector
+function findElementsInShadowTree(root, selector) {
const shadowElements = [];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
- let node = walker.nextNode();
+ /** @type {Node|null} */
+ let node = walker.currentNode;
while (node) {
if (node instanceof HTMLElement && node.shadowRoot) {
shadowElements.push(...node.shadowRoot.querySelectorAll(selector));
@@ -13408,6 +13421,52 @@ function findEnclosedElements(root, selector) {
return shadowElements;
}
+/**
+ * The function looks for form's control elements, and returns them if they're iterable.
+ * @param {HTMLElement} form
+ * @param {string} selector
+ * @returns {Element[]|null}
+ */
+function getFormControlElements(form, selector) {
+ // Some sites seem to be overriding `form.elements`, so we need to check if it's still iterable.
+ if (form instanceof HTMLFormElement && form.elements != null && Symbol.iterator in Object(form.elements)) {
+ // For form elements we use .elements to catch fields outside the form itself using the form attribute.
+ // It also catches all elements when the markup is broken.
+ // We use .filter to avoid specific types of elements.
+ const formControls = [...form.elements].filter(el => el.matches(selector));
+ return [...formControls];
+ } else {
+ return null;
+ }
+}
+
+/**
+ * Default operation: finds elements using querySelectorAll.
+ * Optionally, can be forced to scan the shadow tree.
+ * @param {HTMLElement} element
+ * @param {string} selector
+ * @param {boolean} forceScanShadowTree
+ * @returns {Element[]}
+ */
+function queryElementsWithShadow(element, selector) {
+ let forceScanShadowTree = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+ /** @type {Element[]|NodeListOf} element */
+ const elements = element.querySelectorAll(selector);
+ if (forceScanShadowTree || elements.length === 0) {
+ return [...elements, ...findElementsInShadowTree(element, selector)];
+ }
+ return [...elements];
+}
+
+/**
+ * Checks if there is a single username-like identity, i.e. email or phone
+ * @param {InternalIdentityObject} identities
+ * @returns {boolean}
+ */
+function hasUsernameLikeIdentity(identities) {
+ return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone);
+}
+
},{"./Form/matching.js":34,"./constants.js":57,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],55:[function(require,module,exports){
"use strict";
@@ -13447,7 +13506,8 @@ Object.defineProperty(exports, "__esModule", {
});
exports.DDG_DOMAIN_REGEX = void 0;
exports.createGlobalConfig = createGlobalConfig;
-const DDG_DOMAIN_REGEX = exports.DDG_DOMAIN_REGEX = new RegExp(/^https:\/\/(([a-z0-9-_]+?)\.)?duckduckgo\.com\/email/);
+/* eslint-disable prefer-const */
+const DDG_DOMAIN_REGEX = exports.DDG_DOMAIN_REGEX = /^https:\/\/(([a-z0-9-_]+?)\.)?duckduckgo\.com\/email/;
/**
* This is a centralised place to contain all string/variable replacements
@@ -13828,25 +13888,25 @@ const contentScopeSchema = exports.contentScopeSchema = null;
const userPreferencesSchema = exports.userPreferencesSchema = null;
const outgoingCredentialsSchema = exports.outgoingCredentialsSchema = null;
const availableInputTypesSchema = exports.availableInputTypesSchema = null;
-const availableInputTypes1Schema = exports.availableInputTypes1Schema = null;
-const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = null;
-const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = null;
-const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = null;
-const storeFormDataSchema = exports.storeFormDataSchema = null;
-const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = null;
const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = null;
const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultSchema = null;
-const autofillSettingsSchema = exports.autofillSettingsSchema = null;
+const availableInputTypes1Schema = exports.availableInputTypes1Schema = null;
+const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = null;
+const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = null;
const emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = null;
const emailProtectionGetUserDataResultSchema = exports.emailProtectionGetUserDataResultSchema = null;
const emailProtectionGetCapabilitiesResultSchema = exports.emailProtectionGetCapabilitiesResultSchema = null;
const emailProtectionGetAddressesResultSchema = exports.emailProtectionGetAddressesResultSchema = null;
const emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtectionRefreshPrivateAddressResultSchema = null;
-const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = null;
-const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = null;
-const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = null;
+const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = null;
+const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = null;
+const storeFormDataSchema = exports.storeFormDataSchema = null;
+const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = null;
const askToUnlockProviderResultSchema = exports.askToUnlockProviderResultSchema = null;
const checkCredentialsProviderStatusResultSchema = exports.checkCredentialsProviderStatusResultSchema = null;
+const autofillSettingsSchema = exports.autofillSettingsSchema = null;
+const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = null;
+const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = null;
const apiSchema = exports.apiSchema = null;
},{}],60:[function(require,module,exports){
@@ -13964,7 +14024,7 @@ function waitForResponse(expectedResponse, config) {
return;
}
try {
- let data = JSON.parse(e.data);
+ const data = JSON.parse(e.data);
if (data.type === expectedResponse) {
window.removeEventListener('message', handler);
return resolve(data);
@@ -14119,7 +14179,7 @@ async function extensionSpecificRuntimeConfiguration(deviceApi) {
return {
success: {
// @ts-ignore
- contentScope: contentScope,
+ contentScope,
// @ts-ignore
userPreferences: {
// Copy locale to user preferences as 'language' to match expected payload
@@ -14313,6 +14373,7 @@ function waitForWindowsResponse(responseId, options) {
if (options?.signal?.aborted) {
return reject(new DOMException('Aborted', 'AbortError'));
}
+ // eslint-disable-next-line prefer-const
let teardown;
// The event handler
@@ -17199,7 +17260,6 @@ exports.default = void 0;
window.requestIdleCallback = window.requestIdleCallback || function (cb) {
return setTimeout(function () {
const start = Date.now();
- // eslint-disable-next-line standard/no-callback-literal
cb({
didTimeout: false,
timeRemaining: function () {
diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/index.css b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/index.css
similarity index 100%
rename from node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/index.css
rename to node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/index.css
diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/index.js b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/index.js
similarity index 99%
rename from node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/index.js
rename to node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/index.js
index 4999e435b990..8119bf66cd0a 100644
--- a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/index.js
+++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/index.js
@@ -1942,7 +1942,7 @@
return null;
}
- // pages/duckplayer/src/locales/en/duckplayer.json
+ // pages/duckplayer/public/locales/en/duckplayer.json
var duckplayer_default = {
smartling: {
string_format: "icu",
@@ -2004,7 +2004,7 @@
this.focusMode = focusMode;
}
/**
- * @param {keyof import("../../../types/duckplayer").DuckPlayerPageSettings} named
+ * @param {keyof import("../types/duckplayer.js").DuckPlayerPageSettings} named
* @param {{state: 'enabled' | 'disabled'} | null | undefined} settings
* @return {Settings}
*/
@@ -2092,12 +2092,12 @@
};
}
var MessagingContext2 = G(
- /** @type {import("../src/js/index.js").DuckplayerPage} */
+ /** @type {import("../src/index.js").DuckplayerPage} */
{}
);
var useMessaging = () => x2(MessagingContext2);
var TelemetryContext = G(
- /** @type {import("../src/js/index.js").Telemetry} */
+ /** @type {import("../src/index.js").Telemetry} */
{}
);
var useTelemetry = () => x2(TelemetryContext);
@@ -2858,7 +2858,7 @@
}
};
- // pages/duckplayer/src/js/utils.js
+ // pages/duckplayer/src/utils.js
function createYoutubeURLForError(href, urlBase) {
const valid = VideoParams.forWatchPage(href);
if (!valid) return null;
@@ -3360,7 +3360,7 @@
}
}
- // pages/duckplayer/src/js/storage.js
+ // pages/duckplayer/src/storage.js
function deleteStorage(subject) {
Object.keys(subject).forEach((key) => {
if (key.indexOf("yt-player") === 0) {
@@ -3391,7 +3391,7 @@
});
}
- // pages/duckplayer/src/js/index.js
+ // pages/duckplayer/src/index.js
var DuckplayerPage = class {
/**
* @param {import("@duckduckgo/messaging").Messaging} messaging
@@ -3403,7 +3403,7 @@
/**
* This will be sent if the application has loaded, but a client-side error
* has occurred that cannot be recovered from
- * @returns {Promise}
+ * @returns {Promise}
*/
initialSetup() {
if (this.injectName === "integration") {
@@ -3427,7 +3427,7 @@
/**
* This is sent when the user wants to set Duck Player as the default.
*
- * @param {import("../../../../types/duckplayer").UserValues} userValues
+ * @param {import("../types/duckplayer.ts").UserValues} userValues
*/
setUserValues(userValues) {
return this.messaging.request("setUserValues", userValues);
@@ -3467,7 +3467,7 @@
* }
* ```
*
- * @param {(value: import("../../../../types/duckplayer").UserValues) => void} cb
+ * @param {(value: import("../types/duckplayer.ts").UserValues) => void} cb
*/
onUserValuesChanged(cb) {
return this.messaging.subscribe("onUserValuesChanged", cb);
@@ -3501,7 +3501,7 @@
this.messaging = messaging2;
}
/**
- * @param {import('../../../../types/duckplayer').TelemetryEvent} event
+ * @param {import('../types/duckplayer.ts').TelemetryEvent} event
* @internal
*/
_event(event) {
diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/inline.js b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/inline.js
similarity index 90%
rename from node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/inline.js
rename to node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/inline.js
index 6f18bb2bfd2e..41908278e673 100644
--- a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/inline.js
+++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/inline.js
@@ -1,6 +1,6 @@
"use strict";
(() => {
- // pages/duckplayer/src/js/inline.js
+ // pages/duckplayer/src/inline.js
var param = new URLSearchParams(window.location.search).get("platform");
if (isAllowed(param)) {
document.documentElement.dataset.platform = String(param);
diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/mobile-bg-GCRU67TC.jpg b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/mobile-bg-GCRU67TC.jpg
similarity index 100%
rename from node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/mobile-bg-GCRU67TC.jpg
rename to node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/mobile-bg-GCRU67TC.jpg
diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/player-bg-F7QLKTXS.jpg b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/player-bg-F7QLKTXS.jpg
similarity index 100%
rename from node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/js/player-bg-F7QLKTXS.jpg
rename to node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/dist/player-bg-F7QLKTXS.jpg
diff --git a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/index.html b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/index.html
index f48bb6eef269..0c4099075b32 100644
--- a/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/index.html
+++ b/node_modules/@duckduckgo/content-scope-scripts/build/android/pages/duckplayer/index.html
@@ -4,11 +4,11 @@
Duck Player
-
-
+
+
-
+